← All posts

Code and API Design

SOLID principles applied with restraint, design patterns without cargo-culting, the timing of abstraction, and designing the contracts where teams integrate.

architecture
tech-lead
engineering

Code and API Design

Architecture is the shape of the system at a distance. Design is the shape up close — how classes, modules, and interfaces are structured so the code stays changeable. The same theme from earlier chapters applies here at a smaller scale: it's all about managing coupling and cohesion. But code-level design has a distinctive trap that's worth naming up front: the failure mode here is as often too much structure as too little. Premature abstraction and pattern-cargo-culting cause as much damage as having no structure at all.

SOLID, held lightly

SOLID is five principles for object-oriented design. They're genuinely useful as lenses, dangerous as commandments. Briefly:

The honest framing for a lead: SOLID describes properties that well-factored code tends to have. It does not mean "add an interface and a factory for everything." Applied dogmatically, it produces a maze of indirection where you open six files to follow one operation. Use the principles to diagnose code that's painful to change ("this is hard to test → maybe we're not inverting dependencies"), not as a checklist to apply preemptively.

Design patterns without the cargo cult

Design patterns are named solutions to recurring design problems — Strategy, Observer, Factory, Adapter, and so on. Knowing them is valuable for two reasons: they're genuinely good solutions, and they give your team a shared vocabulary ("let's make this a strategy" communicates a whole design in three words).

The trap is using patterns as a sign of sophistication rather than a response to a real problem. A codebase where every simple thing is wrapped in a factory producing a strategy injected by a builder is not sophisticated; it's a junior engineer who just read the patterns book. The mature instinct: reach for a pattern when you feel the specific pain it solves, not before. Most code needs far fewer patterns than pattern enthusiasts apply.

The timing of abstraction

This deserves emphasis because getting it wrong is so common. An abstraction has a cost (indirection, a thing to understand) and a benefit (it absorbs change in one place). The benefit only materializes when change actually arrives in the shape you predicted.

The two opposing mistakes:

Two ways to get abstraction wrongPremature abstractionBuilding a flexible, generalized frameworkfor variation that never arrives — orarrives in a shape you didn't predict.Cost: indirection & complexity now,benefit later (maybe never). Thewrong abstraction is costlier than none.Copy-paste sprawlThe same logic duplicated in many placesbecause no one pulled out the shared ideaeven after it clearly recurred.Cost: a change must be made in N places;someone always misses one. Bugs breedin the copies that drift out of sync.

A practical heuristic: prefer duplication until the pattern is clear. The "rule of three" is a decent guide — by the third time you write something similar, you understand the real shape of the variation well enough to abstract it correctly. Abstracting after the first occurrence is guessing; abstracting after the third is responding to evidence. As the saying goes, a wrong abstraction is more expensive than a little duplication — because duplication is easy to see and consolidate later, while a wrong abstraction forces everything through a shape that doesn't fit.

API and contract design

For a lead, this is the highest-stakes part of code-level design, because APIs are where teams and services integrate — they're the contracts that, once published, are expensive to change. A bad internal function is a private problem; a bad public API contract is everyone's problem, possibly for years.

Principles that matter:

Design the contract, not the implementation. A good API exposes what a caller can do and hides how it's done. Callers should be able to depend on the contract while you freely change everything behind it. Leaking implementation details into the interface (exposing your database column names as API fields, say) recouples consumers to your internals.

Make contracts hard to misuse. Clear names, explicit required vs optional fields, sensible defaults, and errors that tell the caller what to do. The best APIs make the right thing easy and the wrong thing difficult or impossible.

Plan for change with versioning and backward compatibility. You will need to evolve an API after others depend on it. The discipline: additive changes are safe (new optional fields, new endpoints); removing or changing the meaning of existing fields is breaking and needs a version strategy and a migration path. A useful mental model is Postel's robustness principle — be conservative in what you send, liberal in what you accept — though apply it thoughtfully, since being too liberal in what you accept can mask integration bugs.

flowchart LR
  C[Consumers] -->|depend only on| Contract[/Stable API contract/]
  Contract --- Impl[Implementation:<br/>free to change behind contract]
  Contract -.->|additive change:<br/>safe| V2[New optional field]
  Contract -.->|breaking change:<br/>needs versioning| Vx[Removed/renamed field]

Think about the consumer's experience. The best test of an API is to write the client code first, before the implementation — you immediately feel the awkwardness of a bad design when you're the one calling it.

What this means for you as a lead

In code review, watch for both failure modes. When you see indirection that isn't paying for itself — abstractions with a single implementation, patterns applied to problems that don't have them — gently ask "what variation is this absorbing?" When you see the same logic copied a third time, that's the moment to consolidate. And spend disproportionate care on API contracts: they're the cheapest thing to get right early and among the most expensive to fix once others depend on them. A reviewable rule of thumb — if this were published and a hundred teams depended on it, would we still be comfortable? — calibrates how much rigor a given interface deserves.