Clean Code for Humans and LLMs (Without Killing the Joy of Coding)
Source: Dev.to
LLM‑friendly Clean Code
LLMs can write code. They can refactor. They can generate tests.
If you’ve been coding for years, it’s easy to feel uneasy — not because the tool is bad, but because it touches something personal: a big part of coding joy comes from cleverness.
- From elegant solutions, from seeing a problem collapse into a clean abstraction.
- And then you read advice like: “Prefer boring over clever.”
- It sounds like an obituary for creativity.
It doesn’t have to be that way. This article argues two things:
- Clean code for humans and clean code for LLMs are slightly different (human readability vs. model ambiguity).
- We can reconcile them without sacrificing the pleasure of coding, by placing cleverness where it belongs.
Context vs. Prediction
Humans read code with context
- team conventions
- architectural history
- domain knowledge
- intuition about “how we do things here”
LLMs don’t have that context.
They work by prediction, not intent.
So LLM‑friendly code is code that:
- minimizes ambiguity
- has explicit contracts
- is structurally regular
- is safe to refactor locally
This is not “writing for machines”.
It’s writing code that survives refactors — human or automated.
The phrase “writing for machines” is misleading because it suggests:
- cleverness is bad
- elegance is dangerous
- fun is unprofessional
But cleverness isn’t the enemy – misplaced cleverness is.
Two Kinds of Cleverness
| Good cleverness | Bad cleverness |
|---|---|
| reduces cognitive load | compresses logic into tricky expressions |
| models the domain | uses language edge‑cases |
| creates stable abstractions | depends on implicit assumptions |
| simplifies future change | looks impressive but is fragile |
| Engineering art | Fun for the author, painful for everyone else |
The goal isn’t to ban cleverness – it’s to move it to the right layer: clever inside, boring outside.
The Reconciliation
- Human clean code
- LLM clean code
- Joy of building beautiful things
All three can coexist when we make the outside explicit, stable, predictable, and easy to refactor, while keeping the inside elegant, expressive, and domain‑focused.
Examples
1️⃣ Ruby Service with a Stable Signature
class CreateOrder
def self.call(input)
new.call(input)
end
def call(input)
input = Input.new(input)
validate(input)
.bind { persist(input) }
.bind { publish_event(input) }
end
end
- Predictable
.call - Explicit input coercion
- Clear pipeline
- No metaprogramming
Result type (good cleverness)
class Result
def self.ok(value = nil) = new(true, value, nil)
def self.err(error) = new(false, nil, error)
attr_reader :value, :error
def initialize(ok, value, error)
@ok = ok
@value = value
@error = error
end
def ok? = @ok
def bind
return self unless ok?
yield(value)
end
end
Validation step
def validate(input)
return Result.err(:missing_customer) if input.customer_id.nil?
Result.ok(input)
end
Domain‑friendly, composable pipeline → humans get elegance through composition, LLMs get explicit contracts and safe‑refactor boundaries.
2️⃣ Go HTTP Handler – Explicit & Dumb
type Server struct {
Orders *OrdersService
}
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
order, err := s.Orders.Create(r.Context(), req.ToInput())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(order)
}
LLM can refactor this safely.
Domain service – where the joy lives
type OrdersService struct {
Repo OrdersRepo
Clock Clock
}
func (s *OrdersService) Create(ctx context.Context, in CreateOrderInput) (*Order, error) {
if err := in.Validate(); err != nil {
return nil, err
}
order := NewOrder(in.CustomerID, s.Clock.Now())
order.AddItem(in.ItemID, in.Qty)
if err := s.Repo.Save(ctx, order); err != nil {
return nil, err
}
return order, nil
}
Domain model, invariants, high cohesion → IO is boring; domain is art.
3️⃣ TypeScript – Types Reduce Ambiguity
// Explicit domain result
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
Public function signature
export async function createOrder(
input: CreateOrderInput
): Promise<Result<Order, { code: string }>> {
if (!input.customerId) {
return { ok: false, error: { code: "MISSING_CUSTOMER" } };
}
const order = buildOrder(input);
await repo.save(order);
return { ok: true, value: order };
}
Explicit, easy to consume, hard to misuse, trivial for LLMs to extend safely.
Strongly‑typed domain model
type Money = { currency: "PLN" | "EUR"; cents: number };
function addMoney(a: Money, b: Money): Money {
if (a.currency !== b.currency) throw new Error("currency mismatch");
return { currency: a.currency, cents: a.cents + b.cents };
}
Invariant enforcing, minimal surface area, strong semantics → “beautiful coding”.
Practical Standard You Can Adopt
| Guideline | Why |
|---|---|
| Stable function signatures (keyword args / typed structs) | Guarantees a contract that LLMs can rely on |
Explicit outputs (Result, typed errors) | Removes ambiguity |
| Clean abstractions (small DSLs inside the module) | Keeps cleverness localized |
| Composition pipelines | Allows algorithmic elegance without leaking complexity |
| If you must use metaprogramming or DSLs: • place them in dedicated folders/modules • document WHY | Prevents accidental spread of fragile tricks |
| Write tests like it’s a mini‑framework | Tests act as a safety net for both humans and LLMs |
Not because “short is bad”, but because compressed code is harder to modify safely. LLMs refactor structure, not intent. DRY is a guideline, not a religion.
TL;DR
- Cleverness belongs inside – where it models the domain, reduces cognitive load, and creates stable abstractions.
- Boring, explicit code belongs outside – stable signatures, clear contracts, regular structure.
When we respect this boundary, we get:
- code that is easy for humans and LLMs to navigate,
- safe refactorability,
- and the joy of building beautiful, creative solutions.
Repeat intent if it prevents misinterpretation.
If coding joy came from:
- writing every line by hand
- showing off syntactic wizardry
- squeezing 10 ideas into 1 line
…then yes, some of that fades.
But the deeper joy remains — and grows:
- modeling domains
- designing APIs
- creating abstractions
- reducing complexity
- making systems resilient
LLMs don’t replace that. They amplify it.
“Prefer boring over clever” is incomplete advice.
A better version is:
- Prefer boring where change happens often.
- Prefer clever where meaning lives.
Or, simply:
Clever inside, boring outside.
That’s clean code for humans.
That’s clean code for LLMs.
And it keeps the joy of coding alive.