Learn Software Engineering & Design

How changing requirements become software that stays understandable, testable and safe to change. Thirty-eight modules, from what actually makes code hard to change to designing systems that call a model.

RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

Engineering Fundamentals

7 lessons

What actually makes software hard to change, and why the cost of the next change — not elegance, not pattern count — is the thing design is optimising.

What Makes Software Hard to Change
▶ lab

Not size, not age, and not ugliness. A change is expensive when the knowledge it touches is spread across places that do not know about each other.

Q · Two codebases are the same size and one is a nightmare to change. What is actually different about it?
The Cost of Change

Design decisions are bets on which changes arrive. The bet is priced in indirection now against edits later, and it can lose.

Q · How do I decide whether a structural change is worth making, before I know which requirements are coming?
The Design Loop
▶ lab

Requirement, constraints, invariants, responsibilities, boundaries, interfaces, state, dependencies, failure, implementation, tests, feedback, evolution — in that order, because each answer constrains the next.

Q · What is the general shape of a design decision, so I can ask the same questions of any feature?
Design, Architecture and System Design

Three different grains, three different reversibility profiles. Confusing them is why teams argue about folder layout as though it were a scaling decision.

Q · Where does code design stop and architecture begin, and does the distinction actually matter?
Local Reasoning

Whether you can understand one piece of code without loading the rest of the system into your head. It is the property that decides how a codebase feels to work in.

Q · Why does one codebase feel workable at 200,000 lines and another feel unmanageable at 20,000?
Changeability Is the Goal

Design has no intrinsic virtue. Every structural claim in this domain has to cash out as a change that got cheaper, or it is decoration.

Q · How do I tell a good design argument from an aesthetic preference?
When Design Does Not Pay

Structure is an investment against future change. Where there is no future, it is pure cost — and knowing which code that is, is part of the skill.

Q · When is the right amount of design close to none?

Requirements

6 lessons

Design starts from what must be true, not from choosing a pattern. Functional and non-functional requirements, and the constraints a valid design cannot ignore.

Requirements Before Design
▶ lab

Design starts from what must be true, not from picking a structure. Seven questions decide almost everything that follows, and only one of them is about the happy path.

Q · A ticket arrives. What do I need to know before I can say what the code should look like?
Functional and Non-Functional Requirements

"A user can create an order" fits inside almost any structure. "Order creation is idempotent and auditable" fits inside very few — which is why the second kind decides the design.

Q · Both kinds of requirement are real, so why does one of them do most of the work in deciding the structure?
Constraints Are Part of the Design

The database that already exists, four engineers, a date, a compliance regime and a legacy integration are not obstacles in front of the design. They are inputs to it, and a design that ignores them is not a design.

Q · The textbook answer does not fit my team, my deadline or my existing system. Which one of us is wrong?
The Requirements Nobody States

Timezones, concurrency, partial failure, retention and tenancy are almost never written in the ticket, are almost always real, and are structural — which is the worst combination available.

Q · Which requirements will I discover late, and why is that specific set always the same one?
Requirements Are a Snapshot
▶ lab

You were handed today's version. Which parts of it are stable and which are volatile is not a product question — it is the design input that decides what you hide behind what.

Q · I cannot predict future requirements, so how is "what will change" supposed to be an input to a design I make today?
Design for the Known, Name What You Assumed

You cannot design for requirements you do not have. You can design for the ones you do, and write down the assumptions you made — which is the difference between a decision and a habit.

Q · If speculative design is a trap and unnamed assumptions are also a trap, what is left?

Invariants

5 lessons

The things that must never stop being true, and the engineering question that follows: which layer is actually responsible for protecting each one.

Invariants
▶ lab

A balance that cannot go negative, a username that is unique, an order that cannot ship before payment, a tenant that cannot see another tenant. These are not features — they are the properties everything else is built on top of.

Q · What is the difference between a rule my code checks and a property my system guarantees?
Where Invariants Live
▶ lab

Every invariant is enforced somewhere specific — a type, domain logic, a transaction, a database constraint, an API contract — and the design question is which, because each covers a different set of paths at a different price.

Q · I have written the rule down. Which layer is actually responsible for making sure it holds?
Enforcing Invariants

Types, runtime guards, database constraints and tests are four different mechanisms with four different coverages. Using all four is defence in depth, and it means four places to keep in sync — which is a trade, not a free win.

Q · Should I enforce this rule in one place or in every place I can, and what does the second option actually cost?
Invariant Leaks
▶ lab

The rule is enforced in the service. Then a background job, an admin tool or a migration writes straight to the table. This is the characteristic failure of the whole module, and it is an ownership failure before it is a technical one.

Q · The rule is correctly implemented and the data is still wrong. Where did the guarantee go?
Consistency Boundaries
▶ lab

Which set of things must change together, atomically, for an invariant to hold. Answer that and you have chosen your aggregates, your transactions and — later — where a service could ever be split.

Q · Which pieces of state have to move as one, and what happens to the ones I decide do not?

Problem Decomposition

7 lessons

Splitting a problem by responsibility rather than by folder name — and the single-responsibility idea stated as "a coherent reason to change" rather than "one thing".

Problem Decomposition
▶ lab

Split a problem into responsibilities and give each one an explicit interface. Splitting it into folders named after technical types is not decomposition — it is filing.

Q · A feature is too big to hold in one head. How do I split it so the pieces are genuinely easier than the whole?
Designing by Responsibility
▶ lab

Ask of every unit: what is this responsible for? If the answer needs the word "and" more than once, you have found the design problem before it found you.

Q · What question do I ask of a class, module or function to find out whether it is well designed?
Single Responsibility, Carefully

Not "a class does one thing" — that phrasing has no content. "A module should have one coherent reason to change" has some, and even then the hard cases are genuinely ambiguous.

Q · What does "single responsibility" actually mean, and how do I use it without shattering a codebase into pieces nobody can follow?
Separation of Concerns

Transport, business logic, persistence, formatting and infrastructure change for different reasons, so mixing them is expensive. Adding a layer for each of them anyway is a different and equally expensive mistake.

Q · Which concerns genuinely deserve to be separated, and how do I tell that apart from adding layers because separation sounds like a virtue?
Decomposition by Folder

The anti-lesson. Splitting by technical type puts every file of a kind together and every file of a feature apart, producing boundaries that no requirement respects.

Q · Why does a codebase organised into neat `controllers/`, `services/` and `models/` folders still make every change touch six files?
Finding Seams
▶ lab

Three probes locate where a boundary should go: follow the change, follow the invariant, follow the rate of change. All three are questions about evidence, not taste.

Q · I accept that boundaries should contain change. How do I find out where the boundary actually belongs, rather than guessing?
Over-Decomposition
▶ lab

Ten files that must all be read together are worse than one file that need not be. Splitting has a cost, it is paid by every future reader, and nothing about it is free.

Q · How do I tell a split that contains change from one that merely spreads it across more files?

Abstraction

6 lessons

An abstraction hides detail behind a useful model, and charges indirection, vocabulary and leakage for it. When to pay, and when duplication is cheaper.

What an Abstraction Actually Is

A useful model that lets a caller ignore something specific. `PaymentGateway.charge()` hides provider HTTP; `UtilityManagerFactoryHelper` hides nothing and is therefore not an abstraction at all.

Q · What distinguishes a real abstraction from a wrapper with an important-sounding name?
What an Abstraction Costs

Indirection, vocabulary, learning cost and leakage — charged to every future reader, forever, whether or not the flexibility is ever used. Do not abstract by default.

Q · What does adding an abstraction cost, and who pays it?
The Rule of Three

A heuristic about evidence, not a counting rule. Duplicate until the pattern is a pattern, because the third case is usually the first one that shows you which parts actually vary.

Q · How much evidence should I have before turning duplication into an abstraction?
Leaky Abstractions

`Repository.save()` claims database independence while transaction scope, isolation level, index behaviour and failure modes come straight through. An abstraction hides a mechanism; it cannot erase the physics underneath it.

Q · Why do abstractions keep failing exactly when they matter most, and what should I do about it?
Premature Abstraction
▶ lab

The wrong abstraction costs more than the duplication it replaced, because duplication is visible and a wrong shared unit is not. Its signature is callers that diverged and a parameter list that grew flags to hold them together.

Q · Why is a wrong abstraction more expensive than the duplication it removed, and what do I do once I have one?
Choosing the Model
▶ lab

An abstraction buys flexibility along one axis and pays along every other. Which axis you pick is not a detail of the design — it is the design.

Q · Given that any abstraction makes one kind of change cheap and other kinds more expensive, how do I choose which kind?

Encapsulation & Information Hiding

5 lessons

Keeping implementation behind stable boundaries, and the sharper idea underneath it: hide the decisions most likely to change.

Cohesion & Coupling

7 lessons

The five kinds of coupling that actually differ in cost, why cohesion is the other half of the same question, and how fan-in, fan-out and cycles show up in a real dependency graph.

Cohesion

A module is cohesive when its parts change for the same reason. Cohesion and coupling are one question asked twice: what belongs together, and what may know about what.

Q · What belongs in this module, given that anything I take out of it becomes a dependency between two modules?
Kinds of Coupling

Data, control, temporal, shared-state and implementation coupling are not degrees of one thing. They differ by an order of magnitude in cost, which is why the taxonomy is worth having.

Q · Two modules are connected. Which kind of connection is it, and what will it cost me when a requirement changes?
Temporal Coupling

When calls must happen in an order the type system does not know about, the ordering lives in someone's head — and heads leave.

Q · How do I design an interface so that the invalid call order cannot be written, rather than merely documented?
Shared-State Coupling

Two modules connected through a mutable structure neither of them owns. The most expensive kind, because there is no list of who writes to it.

Q · Two modules never call each other and still break each other. What connects them, and how do I make that connection visible?
Fan-in and Fan-out

How many modules depend on this one, and how many does it depend on. Both are signals about where change lands — and neither, on its own, is a verdict.

Q · A module has a lot of dependents, or a lot of dependencies. Which of those is a problem, and how would I tell?
Dependency Cycles

A cycle turns three modules into one. You cannot reason about, test, initialize, extract or delete any of them without the others.

Q · Three modules each call the next and the last calls the first. What has that actually cost me?
Afferent and Efferent Coupling

Who depends on me, and what do I depend on. A useful pair of questions, a widely published pair of metrics, and a gap between the two that is worth being honest about.

Q · Given who depends on this module and what it depends on, what can I actually conclude — and what am I about to over-conclude?

Dependency Design

7 lessons

Direction, inversion and injection — taught as three different ideas rather than one, because conflating them is how teams end up with a container and no boundaries.

Dependency Direction

Stable, high-level policy should not depend unnecessarily on volatile, low-level detail. The word "unnecessarily" is doing almost all the work.

Q · When module A and module B have to talk to each other, which one should know the other exists?
Dependency Inversion

Business policy defines the interface; infrastructure implements it. Both arrows point at the middle — and none of this requires a container.

Q · How do I let a business rule trigger an effect it must not know the mechanism of?
Dependency Injection

An object receives its collaborators instead of constructing them. That is the whole idea, it needs no library, and it is the third of three things people call DIP.

Q · Should an object build the things it needs, or be handed them?
Constructor Injection

Take collaborators as constructor parameters and the type system enforces that a constructed object is a usable one. No framework required, and the parameter count is a design signal.

Q · Of all the ways to hand an object its collaborators, which should be the default and why?
Service Locator

A global registry objects pull dependencies out of. It makes dependencies implicit — which defeats local reasoning and moves whole classes of error from compile time to run time.

Q · Why is asking a registry for a dependency worse than being handed it, when the object ends up with the same collaborator either way?
Wiring and the Composition Root

Somebody has to construct the object graph. Doing it in one deliberate place is the design decision; doing it with a container is a separate, later, optional one.

Q · If nothing constructs its own dependencies, who constructs anything — and where does that code live?
Volatile Dependencies

Only some dependencies are worth inverting: the ones that change, that are slow, that have side effects, or that are non-deterministic. The rest should be called directly.

Q · Which of the things this module depends on actually deserve an interface, and which should I just call?

SOLID, Critically

7 lessons

Each principle by the problem it addresses, a real example, a real misuse and a counterexample. Useful heuristics, not laws, and the difference matters.

SOLID, Read Honestly

Five heuristics about the cost of change, collected over two decades, of genuinely unequal quality — and none of them a law.

Q · What do the five principles actually have in common, and how much weight can each one bear?
Single Responsibility, Critically

"A coherent reason to change" is the useful reading. "One thing" is the one that produces a hundred classes that each do nothing.

Q · What counts as one responsibility, when every class can be described as doing one thing or as doing five?
Open/Closed, Critically

Prefer designs where common, observed variation can be added without rewriting stable core logic. Not "never modify existing code" — that reading builds plugin machinery for variation that never arrives.

Q · When is it worth making a piece of code extensible, given that extensibility is only ever cheap for the variation you predicted?
Liskov Substitution, Critically

A subtype must keep every promise callers rely on from the abstraction. It is a behavioural contract, not a fact about inheritance syntax — and it is the sharpest of the five.

Q · What exactly does a caller rely on when it holds a reference to an abstraction, and how would I know if an implementation broke it?
Interface Segregation, Critically

A consumer should depend on the smallest contract that serves it. That is a statement about coupling, not a rule that interfaces must be small.

Q · How wide should an interface be, when narrowing it means more types and widening it means more consumers affected by every change?
Dependency Inversion, Critically

The idea is which module declares the interface. The cargo cult is a container, an interface per class, and the same dependency graph as before.

Q · We installed a DI container and registered everything. Have we inverted any dependencies?
How SOLID Gets Misused

An interface per class, layers that pass data unchanged, abstractions with one implementation — and the critiques of SOLID that are strong enough to deserve a straight answer.

Q · This codebase followed the principles carefully and is miserable to change. What went wrong, and how much of it is the principles' fault?

Composition & Inheritance

5 lessons

Why composition avoids rigid hierarchies, when inheritance genuinely earns its place, and polymorphism as a response to variation that is actually there.

Composition Over Inheritance

A hierarchy picks one axis of variation forever. A field can be swapped. That reversibility — not elegance — is the whole argument.

Q · Why does a three-level class hierarchy get expensive to change when the same behaviour held in fields does not?
When Inheritance Fits

Real substitutability, a closed and stable set of subtypes, and shared behaviour that is genuinely the same behaviour. Miss any one and you have coupled two types to save typing.

Q · Under what conditions is `extends` the right answer rather than the convenient one?
Polymorphism

One interface, several implementations, chosen because the behaviour genuinely differs. With one implementation it is not polymorphism — it is a redirect with a type on it.

Q · When does dispatching on a type buy something that a conditional does not, and when is it the same conditional hidden across six files?
Interface Versus Implementation

Depend on the narrowest thing that does the job. That is a different rule from "declare an interface for everything", and it more often means asking for less than for an abstraction.

Q · What should a function ask for — the concrete type, an interface, or just the two fields it actually reads?
Mixins, Traits and Embedding

Four languages, one problem: share behaviour without spending the inheritance slot. Each solution picks a different thing to give up, and knowing which tells you what the code will do under change.

Q · My language offers traits, mixins or embedding as a third option between inheritance and delegation. What does each one actually cost?

Design Patterns

11 lessons

Each pattern as a response to an observed problem, with its trade-off and its simpler alternative — because a pattern applied without the problem is just indirection.

Patterns as Vocabulary

Their durable value is a shared name for a shape you already built. Treated as a construction kit instead of a naming scheme, they add structure nobody needed.

Q · What are design patterns actually for, given that most codebases would be worse if they contained more of them?
Strategy

Behaviour that varies by a recurring kind, held in a value instead of a conditional. Worth it when the variation is real and stable — and a map of functions usually gets you there first.

Q · A pricing conditional appears in four places. When is that a strategy, and when is it a function I should have passed in?
Factory

Encapsulates a construction decision that callers should not make. When there is no decision, a factory is a function that calls `new` and charges you a file for it.

Q · When does creating an object deserve its own abstraction, and when is `new Thing()` the right answer forever?
Adapter

Your interface on one side, someone else's on the other, and a translation in between. The most consistently useful pattern here, because it is an anti-corruption layer in miniature.

Q · A third-party client does not match how my code thinks. Do I bend my code to it, or put something in between?
Facade

A small interface over a subsystem whose full surface most callers do not need. Useful when the subsystem is genuinely complex and dangerous when the facade becomes the only way in.

Q · Six callers use four of my subsystem's twenty types to do the same three-step thing. Should there be one entry point?
Decorator

Add behaviour by wrapping rather than by editing. Excellent for logging, caching and retry; the cost is a stack trace nobody can read and behaviour that depends on wrapping order.

Q · I need logging, caching and retry around a client. Do I put them inside it, wrap it, or leave them at the call site?
Observer

The producer stops knowing its consumers. That is the point and the price: nothing in the code shows what happens when the event fires.

Q · Order confirmation needs to send an email, update inventory and notify analytics. Should the order module call those, or announce something and let them listen?
Command

An action turned into a value. Pointless if you only intend to call it — and genuinely load-bearing the moment you need to queue, retry, audit, schedule or undo it.

Q · When is it worth representing "do this" as data rather than just doing it?
State Pattern
▶ lab

Behaviour that varies by lifecycle state, held in a type per state. Often the right instinct — and an explicit state machine is usually the clearer way to satisfy it.

Q · My order class is full of `if (status === ...)`. Is a class per state the fix, or is the fix a transition table?
Template Method

A base class fixes the steps and subclasses fill in two of them. It works, and passing the two steps in as functions does the same job without a hierarchy.

Q · Four import jobs share the same seven steps and differ in two. Should the shared part be a base class?
Pattern Overuse
▶ lab

The anti-lesson. Abstractions with one implementation, event buses for local calls, factories of factories — structure added for problems nobody has, and it is not free.

Q · How do I tell a design that is prepared for change from one that has been decorated with structure?

Domain Modeling

10 lessons

Entities, value objects, aggregates and services — plus the honest question of whether a rich domain model earns its cost over a transaction script for the system in front of you.

Domain Modeling
▶ lab

Getting the nouns and verbs the business actually uses into the code, so a requirement in their sentence maps to a change in one of yours.

Q · A requirement arrives in the business's words. How do I make the code contain those words, so translating it is not the expensive part?
Ubiquitous Language

One word for one concept, in conversation and in code — where the concept is real. The failure is three names for one thing across three modules, all of them defensible.

Q · When is it worth forcing the code to use the business's word, and when is a separate technical name the honest one?
Entities

Some things are the same thing after every one of their fields has changed. Order #123 is still order #123 — identity, not equality, is what defines them.

Q · Which of my types are the same thing over time even when their contents change, and what does that force me to design?
Value Objects

Things defined entirely by their value — Money, EmailAddress, Coordinates. The highest-value, lowest-cost idea in this module, and the one worth adopting even if you take nothing else.

Q · Which of my types have no identity at all, and what do I get by giving them a name instead of passing a number around?
Aggregates

A consistency boundary drawn around state that must change together. Powerful and easy to over-apply — most objects are not aggregates and should not be treated as one.

Q · Which pieces of state must be consistent with each other at every instant, and which are merely related?
The Aggregate Root

One door into the boundary. External changes go through the root so the invariant has exactly one place it can be checked — and exactly one place it can be bypassed.

Q · If a rule spans several objects, how do I make it impossible to change any of them without the rule being checked?
Domain Services

For the operations that genuinely belong to no single entity or value object. A small, useful category — and a dumping ground the moment it stops being small.

Q · This behaviour is domain logic and does not fit on any one object. Where does it go, and how do I stop that place becoming everything's home?
The Anemic Domain Model

Data objects with no behaviour, and all the logic in services. Widely called an anti-pattern, widely defended, and correct more often than either side admits.

Q · My entities are data holders and the rules live in services. Is that actually a problem, or only a problem in some systems?
Transaction Script

One procedure per operation, top to bottom, doing the whole job. Often exactly right — and the criteria for when it stops being right are knowable in advance.

Q · When is a straightforward procedure per use case the correct design, and what specifically tells me it has stopped being?
When Domain-Driven Design Does Not Pay
▶ lab

The machinery costs vocabulary, indirection and mapping on every change. It repays only at real domain complexity, with real access to someone who knows the domain.

Q · What has to be true about my system and my team before this module's heavier ideas are worth their cost?

State & State Machines

7 lessons

Making lifecycle explicit, so that invalid transitions become impossible to express rather than merely undesirable — and boolean-flag explosion becomes visible.

Explicit State
▶ lab

Name the states a thing can be in instead of inferring them from combinations of fields. The inference is a rule, and an unwritten rule is enforced by memory.

Q · The lifecycle of this object is currently derived from four timestamps and two flags. What do I gain by naming the states instead?
State Machines
▶ lab

States, transitions, guards and effects as a table the code reads — so the lifecycle is data you can review rather than control flow you have to reconstruct.

Q · How do I express a lifecycle so that the legal moves, their preconditions and their side effects are all visible in one place?
Invalid Transitions
▶ lab

The moves that must not exist are part of the design. A comment saying "do not cancel after delivery" is a hope; a transition table that has no such row is a rule.

Q · How do I make an illegal lifecycle move impossible rather than merely discouraged, and how do I record why it is illegal?
Boolean Flag Explosion
▶ lab

Four independent booleans describe sixteen states. Five are legal. The other eleven are not prevented by anything, and the arithmetic is the whole argument.

Q · My object has `isPaid`, `isCancelled`, `isShipped` and `isRefunded`. How many states does that actually create, and how many of them did anyone design?
State Ownership

Which module is allowed to mutate this piece of domain state — and what it means that the answer is currently "any of them".

Q · Six modules can write this field. Which one is responsible for it being correct, and how would anyone tell?
Making Illegal States Unrepresentable

A model where `status = "paid"` with `paidAt = null` cannot be written at all. Powerful where an invariant justifies it — and easy to overdo on a model that has no such invariant.

Q · This combination of values is always a bug. Can I shape the type so it cannot be written, and is that worth what the shape costs?
Optional Values and Absence

Say "this may be missing" in the type where the language supports it, and never encode absence as an empty string, a zero, a sentinel date or a magic id.

Q · This value may not be there. How do I say that so a caller cannot forget, and what do I do in a language that will not let me?

Error Modeling

6 lessons

Failure as part of the design: separating expected business failure from validation, dependency failure and outright bugs, and choosing how each is expressed in types.

Error Modeling
▶ lab

Failure is part of the design, not an appendix to it. Expected business failure, validation failure, dependency failure and programming bug are four categories with four different correct responses.

Q · Which failures belong in my domain model, which are translation, and which are simply bugs I must never handle?
An Error Taxonomy That Survives Contact

Six kinds — InvalidInput, NotFound, Conflict, Unauthorized, DependencyTimeout, InternalBug — chosen because each one gets a different response. The taxonomy is a type-level decision, not a status-code table.

Q · How many error kinds should my codebase have, and what makes one kind genuinely different from another?
Result Types

`Result<Payment, PaymentError>` puts expected failure in the return type, where the compiler can insist somebody deals with it. What that costs depends enormously on the language.

Q · When is it worth making failure part of the return type rather than a separate control-flow path?
Exceptions, Where They Help and Where They Hide the Flow

A non-local jump is exactly right for a failure nobody local can answer, and exactly wrong for an outcome the caller was supposed to decide about. The dividing line is not a rule about exceptions.

Q · Which failures deserve a non-local jump, and when has an exception become a control-flow mechanism in disguise?
Error Boundaries
▶ lab

Every failure has a point where it stops being handled locally and becomes somebody else's problem. Choosing that point deliberately is a design decision; discovering it in production is not.

Q · Where should a failure stop travelling, and who owns it once it crosses that line?
Swallowed Errors
▶ lab

`catch {}` is the visible version. The interesting one is an interface that can only return success or failure, so the code that half-worked has nowhere honest to put the truth.

Q · Why does a failure get silently absorbed, and what does that tell me about the interface it was absorbed inside?

Side Effects & Immutability

7 lessons

Pure computation versus I/O and mutation, the functional-core / imperative-shell split, and what immutability buys and costs.

Side Effects

Computation returns a value; an effect changes something. Database writes and network calls are the obvious ones — clocks and randomness are the two that make a function look pure and behave otherwise.

Q · What counts as an effect in my code, and which of them are hiding inside functions that look like calculations?
Functional Core, Imperative Shell
▶ lab

Gather the inputs, decide with pure logic, then perform the effects the decision asked for. It makes the interesting part trivially testable, and it charges you for fetching data you might not need.

Q · How do I get the decision logic out of the I/O without ending up with a slower system and a worse database access pattern?
Immutability

A value that cannot change is a value you can reason about once. That buys local reasoning and cheap change detection, and it charges copying, allocation and awkwardness in the places that genuinely want to mutate.

Q · What do I actually get from making a value immutable, and when is the copying not worth it?
Mutability, Used Deliberately

Mutation is not the problem. Mutation of something with no clear owner is. Local ownership, a stated lifecycle and a boundary it does not cross make it the right design surprisingly often.

Q · When is mutable state the correct design, and what has to be true for it to stay correct?
Hidden Global State
▶ lab

A value reachable from everywhere is a dependency nobody declared. It shows up as tests that pass alone and fail together, functions whose behaviour depends on what ran first, and a concurrency bug you cannot reproduce.

Q · What does a global actually cost me, and how do I remove one without rewriting every caller?
Purity and Testing

A pure function needs no setup: you call it and assert. Every line of setup a test requires is the design telling you what that code depends on, which makes test friction the cheapest design signal available.

Q · What is my test setup telling me about the dependencies of the code under test?
Effect Boundaries
▶ lab

Push effects to the edges so the middle can be reasoned about — and know the limit: some domains are effects all the way down, and there the honest design is to make each effect a modelled step rather than to pretend there is a pure core.

Q · How far can I push effects outward before the pushing costs more than it buys?

Naming & Function Design

6 lessons

Names are the API you present to every future reader. Units, roles, side effects — and function design judged by responsibility rather than line count.

Naming

A name is the interface every future reader uses instead of the body. It has to carry domain meaning, role, units and whether calling it changes anything.

Q · What does a name have to carry so that a reader can use the thing correctly without reading its implementation?
Units in Names and Types

A bare number carries no unit, so the unit lives in someone's head. Three rungs — comment, suffix, type — with escalating cost and escalating safety.

Q · When is a unit a comment, when does it belong in the name, and when does it deserve its own type?
Boolean Parameters

`sendEmail(user, true, false)` is unreadable at the call site, and the call site is where every future reader meets it. The fix is a type, not a comment.

Q · What is actually wrong with a boolean parameter, and what should replace it?
Function Design

A function is judged by what it needs, what it returns, what it changes, how many reasons it has to change, and whether its name is true. Length is not on the list.

Q · What makes a function well designed, if not how long it is?
Long Functions

Long is not automatically bad. Ask whether it mixes responsibilities, whether the control flow can be followed, and whether it hides concepts that deserve names.

Q · This function is three hundred lines. Is that a problem, and how would I know?
Naming and Domain Language

When code uses the words the business uses, a reader can apply domain knowledge instead of tracing execution. That is the difference between reading and deducing.

Q · What does a reader gain when the code speaks the same language as the people who asked for it?

Documentation

5 lessons

Comments that explain why, decision records that survive the author, and the failure mode nobody plans for: documentation that has quietly become wrong.

Code Review

6 lessons

What review is actually for once tooling handles formatting, what a useful checklist asks, and why review size predicts defect detection better than reviewer skill.

What Code Review Is For

Correctness, design, maintainability, security and knowledge sharing — five things a person can do. Formatting is not one of them, because a tool already did it.

Q · Once formatters, linters and tests have settled the mechanical questions, what is a human reviewer actually being asked to do?
A Review Checklist Worth Reading

Six questions, in the order attention runs out: behaviour against requirement, invariants moved, failure modes added, simpler alternative, meaningful tests, and whether you could debug it at three in the morning.

Q · What should a reviewer actually ask, and in what order, so the expensive questions get asked before attention runs out?
Review Size

The same reviewer finds real problems in a sixty-line change and waves through a nine-hundred-line one. Diff size predicts what review catches better than almost anything else about the reviewer.

Q · Why does the same person review a small change carefully and a large change not at all — and what do you do when the change genuinely is large?
Tone, Disagreement and Receiving Review

Critique the code and what it will cost, never the person. Then the two harder halves: disagreeing with someone senior, and taking twenty comments on work you were proud of.

Q · How do I say a design is wrong without the author hearing that they are wrong — and what do I do when the person I disagree with outranks me?
What to Automate Out of Review

Formatters, linters, type checkers, tests and static analysis exist so that human attention is spent only on what is arguable. Every mechanical comment a person still makes is a missing rule.

Q · Which review comments should never have to be made by a person again — and which ones look automatable but are not?
Review as Design Feedback — and Why It Arrives Too Late

By the time a change reaches review the boundary already exists. That is why "this belongs somewhere else" is the comment most often agreed with and least often acted on.

Q · Why does a code review almost never move a boundary, even when the reviewer is right and the author agrees?

Refactoring

9 lessons

Changing internal structure without changing observable behaviour — as a disciplined loop with a safety net, not a rewrite that someone called a refactor.

What Refactoring Actually Is

Changing internal structure without intentionally changing observable behaviour. Almost everything called a refactor is something else, and the difference is what makes it safe.

Q · What separates a refactor from a rewrite, and why does the distinction decide how the work should be done?
The Refactoring Loop

Working code, a safety net, one small transformation, verify, repeat. The discipline is entirely in the size of the step and in never being more than one step from working.

Q · How do I restructure code that is in production without ever being in a state I cannot ship or abandon?
Extract Function

Extract to give a meaningful concept a name, not to reduce a line count. The two motivations produce different code, and only one of them helps.

Q · When does pulling a block of code into its own function make the code easier to change, and when does it just move the problem behind a name?
Extract Module

Pull out a module when a set of responsibilities has become cohesive enough to have its own reasons to change — and its own interface that hides them.

Q · How do I know when a group of functions has become a module, rather than just a folder I put them in?
Rename

The highest value-to-risk refactoring there is, and the most neglected. A better name is a better model, and the cost is usually one command.

Q · Why is renaming a thing worth a pull request of its own, when it changes no behaviour at all?
Move Responsibility

If a piece of logic spends its time reaching into another object's data, it probably belongs to that object. Moving it is usually the cheapest coupling reduction available.

Q · How do I tell whether a piece of logic is in the wrong place, and what does moving it actually buy?
Replace Conditional With Polymorphism

Worth doing when the variation is stable, meaningful and repeated across several operations. Not worth doing to most switch statements, where the switch is clearer than what replaces it.

Q · When does turning a conditional into a set of types make the code easier to change, and when is the conditional simply the right answer?
Introduce Parameter Object

Bundle arguments that travel together and mean something together. Bundling them into a vague `Options` bag because there were too many is how a long parameter list becomes an untyped one.

Q · When does grouping arguments into a type make a signature clearer, and when does it just hide the count?
Refactoring Without Tests

Sometimes you have to change code whose behaviour nothing protects. The technique is a small number of provably-safe moves, used to buy a seam, used to get a characterization test in place.

Q · The code has no tests, I cannot add tests without changing it, and I have to change it. Where does that loop break?

Code Smells

9 lessons

Heuristics that point at a possible design problem, each with the case where it is genuinely fine — because a smell is a question, not a verdict.

What a Code Smell Is

A named pattern that raises a question about the design. Roughly half the time the honest answer is "this is fine", and a smell that cannot say when is just taste with a job title.

Q · When a piece of code looks wrong, how do I tell whether it actually is wrong?
God Object

One type with a huge API, a dozen dependencies and a dozen unrelated reasons to change. The finding is the reason count, not the line count — and the fix is rarely a six-way split.

Q · One class is 3,000 lines and everyone touches it. Is that the problem, or a symptom of one?
Shotgun Surgery

One requirement, seven modules, none of which is about that requirement. The code is not badly written — the knowledge has no owner, so every consumer had to learn it.

Q · Why does a one-sentence business change reliably touch six unrelated packages, none of which is named after it?
Divergent Change

One module, many unrelated reasons to change. The exact dual of shotgun surgery: there the knowledge had no home, here one home holds knowledge that does not belong together.

Q · Why is this one file in every pull request, no matter what the pull request is about?
Feature Envy

A function that reaches into another module's data far more than its own. Sometimes the behaviour is in the wrong place; sometimes the other module is a value type and this is exactly right.

Q · This method uses six fields of another object and none of its own. Does the behaviour belong over there?
Primitive Obsession

Money as `number`, email as `string`, a user id as `int`. Meaningful types move a class of mistake from runtime to compile time — and not every string needs a wrapper.

Q · Which of these strings and numbers deserve a type of their own, and which are fine as they are?
Long Parameter List

Eight arguments, four of them booleans. Bundling them into a parameter object makes the call site tidier and changes nothing — the finding is usually a concept that has no name.

Q · This function takes nine arguments. Is the fix a parameter object, or is something missing from the model?
The Utility Dumping Ground

`utils.ts` is not a module, it is the absence of one. Its contents are the pieces of the domain nobody could find a home for, and it grows because it never says no.

Q · Why does every codebase grow a `utils`, `common` or `shared` module, and what is actually wrong with it?
Duplicate Knowledge

Two identical blocks may not be the same concept, and two blocks that look nothing alike may encode the same rule. Textual similarity is the wrong test, and it is the one everybody uses.

Q · These two blocks are identical. Should they be one — and what about the two that are completely different but always change together?

Technical Debt

5 lessons

A choice that raises the cost of future change. Deliberate and bounded is a strategy; accidental and unnamed is the thing that compounds.

What Technical Debt Actually Is

A design or implementation choice that increases the cost of future change. Not ugliness, not old code, not a library you would not have picked — and the metaphor was originally about deliberate shortcuts.

Q · Half the backlog is labelled "tech debt". Which of it is actually debt, and how would we tell?
Deliberate Debt

A shortcut taken knowingly is a strategy — if the trade-off is understood, the impact is bounded, and someone knows the way out. Two of those three are usually missing.

Q · We need to ship in three weeks and the right design takes six. How do we take the shortcut without it becoming permanent?
Accidental Debt

Debt nobody chose: an unclear domain, requirements that moved, a rushed design, missing tests, people who left. It is the larger half, and it is not a moral failure.

Q · Nobody took a shortcut here and the code is still expensive to change. Where did that come from?
Interest: Why Debt Compounds

The shortcut is the principal. The interest is that every later change in that area costs more — and because later changes are built on earlier ones, the cost grows rather than staying flat.

Q · The shortcut cost us a week to take. Why has it cost far more than a week to live with?
The Debt Register

Five fields per entry — Problem, Impact, Risk, Owner, Trigger — and a hard rule about size. A register with two hundred entries is not a register, it is a wishlist nobody reads.

Q · How do we keep track of the debt we are carrying without producing a list that grows forever and gets read never?

Legacy Code

7 lessons

Code that is risky to change because its behaviour is poorly understood or weakly protected — and the characterization-test, seam, small-step loop that makes it safe.

What "Legacy" Actually Means
▶ lab

Legacy code is code that is risky to change because its behaviour and assumptions are poorly understood or weakly protected. Age is a correlate, not the definition.

Q · Which code in this repository is legacy, and what property am I actually measuring when I decide?
Characterization Tests
▶ lab

Tests that record what the code does today — including what it does wrongly — so that a later change has a baseline to be measured against. They assert behaviour, not correctness.

Q · How do I get a safety net around code whose correct behaviour nobody can state?
Seams
▶ lab

A seam is a place where you can change behaviour, or substitute a dependency, without editing the code at that place. Finding one is what makes untestable code testable.

Q · This code reaches out to the network, the clock and the database from inside a nested loop. Where can I get a grip on it without rewriting it?
The Legacy Change Loop
▶ lab

Understand, characterize, seam, small refactor, change behaviour, verify — in that order, because each step is what makes the next one safe rather than brave.

Q · A ticket needs one behaviour changed in code with no tests. What is the actual sequence of moves, and why is that the order?
The Strangler Pattern
▶ lab

Put a routing layer in front of the old system, move one behaviour at a time behind it, expand until nothing is routed to the old system, then retire it. The old and new run together for a long time.

Q · The module is too large and too unprotected to change in place, and a rewrite is too risky. What is the third option?
The Risk in a Rewrite
▶ lab

Rewrites fail for four specific reasons — hidden requirements, the delivery gap, migration complexity and feature-freeze pressure. They are sometimes still the right call, and the conditions are nameable.

Q · What exactly goes wrong in a rewrite, and under what conditions is one actually the better bet?
Incremental Migration
▶ lab

Old and new coexist; you migrate one slice, verify it against reality, and repeat. The design work is choosing the slice and defining what "verified" means.

Q · If old and new have to run side by side for months, what does one increment consist of and how do I know it worked?

Migration & Compatibility

6 lessons

Every change to a running system has an initial state, a transition state, a final state and a way back. Backward compatibility, versioning and data migration.

Designing the Migration
▶ lab

Every change to a running system has an initial state, a transition state, a final state and a way back. The transition state is the one people skip, and it is live in production longest.

Q · I have designed what the system should look like afterwards. What else does a change to a live system need before it is a design?
Backward Compatibility as a Constraint
▶ lab

Old clients, stored data, in-flight events, published APIs, plugins and databases all constrain what today's change may do. Compatibility is not a property of an API; it is a constraint on every edit.

Q · Who is still running the old version of this, and what does that forbid me from doing today?
Versioned Interfaces

An explicit version lets old and new consumers disagree about the contract. It also creates a maintenance obligation that lasts as long as the oldest version you have not managed to kill.

Q · Should this interface carry an explicit version, or should it just never break?
Data Migration
▶ lab

Schema, data and application code are three things that must change in a safe order. Getting the order wrong is the difference between a routine deploy and a restore from backup.

Q · The schema has to change, the existing rows have to change, and the code has to change. What order does that happen in, and what is live in between?
Expand and Contract
▶ lab

Add the new shape, write both, migrate readers, stop writing the old, remove it. The canonical safe sequence, and the reason it works is that every step is individually revertible.

Q · What is the general sequence for replacing one representation with another while everything keeps running?
Feature Flags and What They Cost
▶ lab

A flag decouples deploying code from releasing behaviour, which is genuinely valuable. It also multiplies the state space of the system and creates a cleanup obligation nobody is measured on.

Q · What does adding a flag do to the number of behaviours my system can exhibit, and who removes it?

Evolvability

7 lessons

Change amplification, local reasoning and encapsulation radius — the properties that decide whether the tenth change is as cheap as the first.

Evolvability
▶ lab

A system is evolvable when the changes it is actually likely to receive stay local and understandable. The word "likely" carries the entire claim.

Q · Two systems both work today and both have tests. What makes one of them still cheap to change in three years?
Change Amplification
▶ lab

One requirement changes; count the modules, interfaces, tests and deployments that must move with it. Lower is usually better, and not always.

Q · One sentence changed in the requirements. How many places in the code have to change, and is that number a design failure?
Encapsulation Radius

Pick an implementation decision and ask who has to be told when it changes. The set of modules that notice is the radius, and a smaller one is stronger hiding.

Q · If I change how this module works internally — not what it promises — how far does the shockwave travel?
Extensibility

Extensibility is cheap along one axis and expensive along every other, so the only real question is which variation you have actually observed.

Q · Should I build an extension point here, and if so, for which kind of variation?
Plugin Architecture

A plugin system is right when independent parties genuinely must extend you. What it costs — a frozen API, a lifecycle, isolation and compatibility — is permanent.

Q · We want third parties to extend the product. What are we actually signing up for?
Stability and Dependency Direction

Something many modules depend on is expensive to change. Point dependencies toward the things that change least — and treat every stability metric as a hint, never a measurement.

Q · This module is depended on by twelve others. What does that oblige me to do differently?
Speculative Generality
▶ lab

Machinery built for a variation that never arrived: one implementation behind an interface, a plugin system with no plugins, an event bus for a local call.

Q · How do I tell structure that is paying for itself from structure built for a future that never came?

Module & Package Structure

8 lessons

Grouping by feature or by layer, what each does to change locality, and why cycles are a reasoning problem before they are a build problem.

Package Design

A package is a claim about what changes together. Grouping by domain, feature or capability makes that claim; grouping only by technical type makes no claim at all.

Q · What should decide which folder a file goes in?
Package by Layer

controllers / services / repositories. It is genuinely good at cross-cutting technical change and at being guessable, and it scatters every feature across every folder.

Q · What does a layer-first layout actually buy, and what does it charge?
Package by Feature

orders / payments / users, each with its own layers inside. Requirement-shaped change becomes local, and shared concepts lose their obvious home.

Q · If I group by capability instead of by technical role, what actually gets cheaper — and what gets worse?
Vertical Slices

One folder per use case, containing everything that use case needs. Change locality is close to maximal, and shared concepts have nowhere obvious to live.

Q · If grouping by capability is good, is grouping by individual use case better?
Circular Dependencies

A depends on B depends on C depends on A. What breaks is reasoning first, then initialisation order, then testability, and only last the build.

Q · My build tolerates cycles, so why does a dependency cycle matter?
Breaking Cycles
▶ lab

Move the shared concept, invert a dependency, introduce an interface — or merge two modules that were never really separate. The last one is the most under-used fix.

Q · I have a dependency cycle. Which of the available fixes is the right one here?
Stable Dependencies

Depend toward the things that change less often than you do. It is a statement about rates of change, not about which folder sits lower in a diagram.

Q · Which way should this dependency point, and what makes the other direction wrong?
Module Granularity
▶ lab

How big should a module be? Big enough that a likely change fits inside it, small enough that one person can hold it. "As small as possible" is not an answer to either question.

Q · How do I know whether this module is the right size?

Architecture Boundaries

6 lessons

Layered, hexagonal, clean and onion compared honestly — plus adapters and anti-corruption layers, which are the part that survives whichever style you pick.

Architecture Boundaries

Domain, application, infrastructure and transport is a useful model of where the seams fall inside a codebase. It is one model, not universal truth, and saying so is the lesson.

Q · Where should the boundaries inside a single codebase fall, and does the four-role model actually answer that?
Hexagonal Architecture (Ports and Adapters)

An application core that declares the interfaces it needs, and adapters on the outside that satisfy them. An ordinary dependency-direction choice, worth its cost exactly where the outside varies.

Q · When does inverting a dependency into a port-and-adapter pair actually pay for the interface it costs?
Clean Architecture, and Where It Is Overused

Policy inward, details outward. One approach among several — and the one most often adopted whole, at a ceremony cost nobody prices before committing.

Q · What does "policy inward, details outward" actually buy, and when is the ceremony it charges not repaid?
Onion Architecture

Concentric rings with the domain model at the centre. Nearly the same idea as hexagonal and Clean, drawn differently — and saying so is more useful than pretending they are three schools.

Q · Are hexagonal, Clean and onion three different architectures, or one idea with three diagrams?
Boundary Adapters

Translate external formats into your own model at the edge, once. The rule that keeps a vendor SDK's types from spreading through code that has nothing to do with the vendor.

Q · Where should an external system's data stop being the external system's data and start being ours?
Anti-Corruption Layer

When the other system's concepts are wrong for you — not just its formats — translate the model, not the fields. The layer exists so their vocabulary cannot colonise yours.

Q · What do I do when integrating a system whose model of the domain genuinely conflicts with mine?

Monolith & Modular Monolith

5 lessons

A monolith can be well designed, and usually should be tried first. Internal module contracts, shared libraries, and the `common/` folder as a design failure.

Feature Design

5 lessons

The work that happens before the first line: goal, rules, state changes, interfaces, persistence, errors, observability, tests — and what fails if you skip it.

Designing a Feature Before Writing It
▶ lab

Eight questions stand between a ticket and the first line of code. Skipping one does not remove the decision — it relocates it to whichever branch of the code happens to run first.

Q · A ticket says "let customers pause their subscription". What has to be decided before I open an editor, and in what order?
A Feature Design Template
▶ lab

Twelve fields, filled in for a real feature. The value is not the document — it is that a blank field is visible in a way an unasked question is not.

Q · How do I make the questions I skipped visible to a reviewer, without turning feature work into a documentation process?
Failure-Aware Feature Design

Four questions that change the structure rather than adding a branch: what if the database is gone, the dependency is slow, the request arrives twice, and half the work already succeeded.

Q · The happy path works. What does this feature do when the write fails, the dependency times out, the request repeats, or part of the work has already committed?
Designing the Happy Path Last

Error, repeat and partial-failure behaviour decided first, because a structure built around the success case has no room left for them — and that is where the mess comes from.

Q · Why does error handling always end up feeling bolted on, even on teams that genuinely care about it?
Slicing a Feature

A slice is a thin path through everything that delivers something observable. A layer is a horizontal band that delivers nothing, and calling it a slice is how all the risk ends up in the last week.

Q · How do I break a three-week feature into increments that are each safe to release and useful on their own?

Debuggability by Design

6 lessons

A system that can answer what happened, why, for whom, on which version. Stable ids, deterministic cores, and time and randomness as injected dependencies.

Debuggability by Design

A system should be able to answer what happened, why, for which request and user, on which version, and from which state. None of those are answerable later if the design did not record them.

Q · Six weeks from now, someone asks why this customer was charged on the 3rd. What has to have been designed in for that to be a ten-minute answer rather than a day?
Logging at Boundaries

Log state transitions and external interactions. A log line is an interface with a future reader, and most debug logging is a message the author sent to themselves an hour ago.

Q · Which events deserve a log line, and which ones cost money, hide the useful lines, and tell a future reader nothing?
Stable Identifiers

request_id, order_id, workflow_id. Correlation is a design decision made in the first week or not at all, because an id cannot be added to records that were written without it.

Q · A customer says "it did not work". What single value do they, or support, hand me that finds every record of what happened?
A Deterministic Core

Same inputs, same outputs, every time. A domain core with no ambient time, randomness, I/O or global state can be tested exhaustively, replayed from a log, and reasoned about without running it.

Q · Why can I not reproduce this bug locally, and what would the code have to look like for the answer to be "paste the inputs and run it"?
Time as a Dependency

A `now()` buried in a rule makes the rule untestable and unreproducible. Injecting a clock fixes that and costs a parameter threaded through code that did not want one — which is a real price, not a rounding error.

Q · Which parts of this system need time to be an input rather than something the code reaches for, and where is threading a clock through not worth it?
Randomness as a Dependency

The same argument as the clock, applied to anything that returns a different answer each call: id generation, shuffling, sampling, jitter. Injected, they are reproducible; ambient, they are a bug you cannot re-run.

Q · Which of the non-deterministic values in this code are decisions I will need to reproduce, and which are noise that should stay ambient?

Testing as Design Feedback

7 lessons

Hard-to-test code is usually telling you something about its dependencies. Where to put the boundary, what to double, and when mocks start mirroring implementation.

Testing as Design Feedback

Code that is miserable to test is usually hiding a dependency or mixing two jobs. That is real information — and it is not a licence to bend the production design around a test runner.

Q · This code is painful to test. Is that a testing problem, a design problem, or neither?
What a Unit Is

A "unit" is not a class and not a method. It is a boundary you have chosen to hold stable — which makes choosing it a design decision, not a testing convention.

Q · Which behaviours deserve their own test, and what should the test be allowed to know?
Where a Test Must Be Real

Some abstractions are load-bearing precisely because the thing underneath them is complicated. Replacing those with a double tests your belief about the dependency rather than the dependency.

Q · Which parts of this design are only meaningfully tested against the real thing?
Mocking

Mock at boundaries you have chosen to keep stable. Mock every internal collaboration and the suite becomes a cast of the implementation — and then it argues against the refactoring it was supposed to enable.

Q · Which collaborations deserve a mock, and which ones should the test simply let happen?
Test Doubles, Precisely

Stub, fake, mock, spy and dummy are not synonyms. They differ in what they know and therefore in how they fail — and picking the wrong one is how a suite ends up brittle or blind.

Q · When I substitute a collaborator, what exactly am I substituting — and what does that substitution stop the test from being able to detect?
Contract Tests

A contract test is the thing that keeps a double honest. It is also a design decision: writing one is a declaration that this seam is a contract and not an implementation detail.

Q · Two sides of a boundary each have green tests. What proves they agree?
Property-Based Testing

When a behaviour can be stated as something true of every input, you can test the statement instead of a handful of examples — and being unable to state one is itself a finding about the design.

Q · Can this behaviour be expressed as something that must hold for all inputs, and what does it mean if it cannot?

Designing for Failure

7 lessons

What a design owes once calls can time out, repeat or half-succeed — including idempotency as a property chosen up front rather than retrofitted.

Designing for Failure

A function call either returns or throws. A remote call has a third outcome — you do not know — and an interface designed without a name for it will be wrong in a way no amount of error handling fixes.

Q · What does an interface have to look like once the call can time out, be retried, or half-succeed?
Idempotency by Design

Idempotency is a property of a signature, not a feature you add later. `createPayment(commandId, amount)` has the id in it because the failure model put it there — and no discipline around `createPayment(amount)` can substitute.

Q · What has to be in this operation's signature for repeating it to be safe?
Partial Failure

A local operation is all-or-nothing because the language and the transaction say so. A distributed one is not, and an interface that returns one boolean for five sub-operations is lying about what happened.

Q · This operation does four things across three systems. What should it return when the second one fails?
Retries Are a Property of the Operation

Retry safety is not something a caller can decide. It is a fact about the operation, and it has to be stated in the interface — otherwise every caller is guessing, and some of them will guess wrong.

Q · What must be true about this operation before anyone is allowed to call it twice?
Concurrency by Design

Every piece of shared mutable state is a permanent tax on reasoning. Before reaching for a lock, ask whether ownership can be local, the data immutable, or the operation atomic — those remove the problem instead of managing it.

Q · Can this design avoid needing a lock at all, and what does it cost if it cannot?
The Thread-Safety Contract

Whether a type may be used concurrently is part of its interface. Leaving it unsaid does not make it safe — it makes every caller guess, and the guesses are wrong at different times.

Q · May two callers use this object at the same time, and where does the answer live?
What Changes at the Network Boundary

A function call becomes a message that may be lost, delayed, duplicated or half-processed. "We can split it later" underestimates this, because the cost is not the transport — it is every interface that was designed as if calls always return.

Q · What exactly changes about an interface when the call it makes stops being local?

Designing for Security

5 lessons

Trust boundaries, least privilege and capability-passing as design decisions in the code, distinct from the attacker techniques Security Engineering teaches.

Designing for Security

Five questions — what is trusted, what is untrusted, who may call this, what data is sensitive, where privilege changes — asked before implementation, because afterwards they are structural changes.

Q · Which security decisions are design decisions, made before any code exists, rather than review findings?
Trust Boundaries

A trust boundary is a line in your code, not a line on a network diagram: the place past which data is assumed clean, which is only true if one place made it so.

Q · Where in the code does untrusted data become trusted, and what makes that a place rather than a habit?
Least Privilege as a Design Decision

Least privilege is usually taught as an infrastructure setting. At code granularity it is a parameter type: a function handed a reader cannot write, and that is enforced rather than reviewed.

Q · How do I give a module only the access it needs, in a way the next engineer cannot casually undo?
Capability Passing

Instead of handing a module a service container and hoping, hand it the specific things it may do: CanSendEmail, CanChargePayment. Powerful, honest, and more ergonomic cost than most codebases will accept.

Q · What changes if a module can only perform effects it was explicitly handed, rather than any effect it can reach?
Sensitive State

Which data is sensitive, where it is allowed to travel, and why a type beats a convention: a value that cannot be stringified cannot be logged by accident.

Q · How do I stop sensitive data ending up somewhere nobody designed it to go?

Designing for Cost

5 lessons

Interfaces that hide what they cost — the repository call that loads a million rows, the N+1 that is an API-shape failure before it is a query failure.

Designing for Cost

Do not optimise blindly — and notice that the structural choices deciding allocations, copies, round trips and contention are all made before there is anything to profile.

Q · Which performance decisions are structural, made before any measurement is possible, and which genuinely should wait for a profiler?
Cost-Aware Interfaces

An interface that hides what it costs is a design failure. `findAll()` looks like a getter and may read the whole table; a signature that cannot express a bound cannot be used safely.

Q · How should an interface tell its callers what calling it will cost?
N+1 as a Design Problem

The classic N+1 is treated as a query bug and fixed with an eager-load hint. Often it is an interface that only knows how to answer about one thing at a time, called from a loop that had no alternative.

Q · Why does the same N+1 keep coming back after it is fixed, and what in the design is producing it?
Allocation and Copies

Immutability, boundary adapters and DTO mapping layers all buy reasoning guarantees with copies. That is usually a good trade and it is never a free one, so the design should be able to say what it bought.

Q · Which copies does my design force, what do they buy, and where does that stop being worth it?
Premature Optimization, Reclaimed

The quote is about small efficiencies and it is routinely used to dismiss all performance thinking. Structural cost decisions are not premature; micro-tuning without measurement is.

Q · When is thinking about performance premature, and when is "that is premature optimisation" being used to end a conversation that should be had?

Complexity

7 lessons

Essential versus accidental, simple versus easy, and the maxims — DRY, YAGNI, KISS — restated so they say something falsifiable.

Essential and Accidental Complexity

Some difficulty is the business rule itself and cannot be deleted, only moved somewhere honest. The rest is your encoding of it — and that part is negotiable.

Q · This feature felt ten times harder than the rule it implements. Which part of that difficulty was the problem, and which part did we build ourselves?
Simple Is Not Easy

Easy is about familiarity and how quickly you can start. Simple is about how few things are braided together. They come apart constantly, and most bad designs are the moment someone chose easy and called it simple.

Q · This choice made the feature fast to write and the codebase harder to reason about. What is the property I traded away, and what would I call it?
The Complexity Budget

Every feature spends concepts, states, dependencies and failure modes out of a budget nobody is tracking. Tracking it does not make features cheaper — it makes the price visible while the decision is still open.

Q · We keep saying yes to individually reasonable features and the system is getting hard to hold in one head. What are we actually spending, and how would we know we had run out?
YAGNI, With Its Bill Attached

Do not build features or flexibility for a requirement nobody has. The rule is right often enough to be a default, and its cost is real: the refactor you deferred arrives under deadline pressure.

Q · Someone wants to build for a requirement that has not been asked for. When is refusing that the cheap choice, and what am I agreeing to pay if I refuse?
KISS: Simplest for the Requirements You Have

The simplest design that satisfies the known requirements — which is a different thing from the smallest amount of code you can write by ignoring some of them.

Q · Two designs, one obviously smaller. How do I tell whether the small one is simple or just leaving the hard cases to whoever hits them?
DRY: Knowledge, Not Lines

The rule is about a piece of knowledge having one authoritative home. It is routinely remembered as a rule about text, and that misreading produces shared abstractions that couple things which have nothing to do with each other.

Q · Two pieces of code look alike. How do I tell whether they are one thing written twice, or two things that happen to resemble each other today?
Over-Design and Under-Design

Two opposite failures with the same cause — structure chosen without reference to expected change. The symptoms are recognisable, and the right amount is a function of how complex the domain is and how often it moves.

Q · One reviewer says this is over-engineered and the author says the last feature built the simple way took three days to change. How do I decide who is right?

Engineering Decisions

8 lessons

Trade-off matrices, decision records with revisit triggers, refactor versus rewrite, build versus buy, and what a framework charges for what it gives.

The Trade-off Matrix

Six axes that real design decisions move, scored side by side — useful because it forces every option to be described on the axes it is worst at, and dangerous because the digits look like evidence.

Q · Three designs, three advocates, and the argument keeps moving. How do I make the comparison concrete without pretending I measured something?
Decision Records

Context, options, decision, why, consequences, revisit trigger. Short enough to write in fifteen minutes, and worth writing because in two years the code will not say what was known when it was chosen.

Q · How do I make a design decision survivable by people who were not in the room, without producing documentation nobody reads?
Revisit Triggers

A decision that was right becomes wrong when its assumptions change, and nobody is watching the assumptions. Naming the evidence that would flip the answer is the cheapest engineering practice there is, and the least used.

Q · This design is correct today. What specific, observable thing would tell us it has stopped being correct — and who would see it?
Refactor or Rewrite

Five questions decide it: can the behaviour be characterized, can the change be incremental, is the architecture salvageable, how much undocumented domain knowledge is in there, and can old and new run side by side.

Q · This code is genuinely bad and the team wants to start again. What would have to be true for that to be the cheaper path, and what does it cost if I am wrong?
Build, Library, SaaS or Managed Service

Four options rather than two, sorted by how much of the thing you end up owning. The question that decides it is whether this capability is what your product is judged on.

Q · We need a capability that thousands of companies also need. Which parts of it should we own, and what are we agreeing to run at 3am?
Library or Framework

A library is something your code calls. A framework is something that calls your code. That inversion — not size, not scope — is what decides how much of your design it gets to make.

Q · Which of these dependencies is deciding my application's structure, and did I agree to that?
What a Framework Charges

Productivity, conventions and an ecosystem, in exchange for its lifecycle, its architecture, an upgrade obligation and the moments its abstractions leak. All four charges are payable later, which is why only the benefits are visible on day one.

Q · We are productive in this framework. What is it charging us, and when does the bill arrive?
Reversible and Irreversible Decisions

Sort decisions by what it costs to undo them, then spend evidence, meetings and caution in proportion. Most decisions are cheap to reverse and are treated as though they were not; a few are permanent and get decided in a chat thread.

Q · How much evidence does this decision deserve — and is that proportional to what it costs to be wrong?

Dependency Management

6 lessons

Direct and transitive dependencies, update risk, API stability, versioning as a communication convention, and deprecation as a lifecycle rather than a deletion.

Dependency Management

Every package in the lockfile is code you now operate without having written it — and most of it arrived without anyone making a decision.

Q · What am I actually taking on when I add a dependency, and how do I keep that decision reversible?
Transitive Dependencies

A small package can carry a large graph you did not choose, cannot audit, and run in production with your own privileges.

Q · I added one package and my install pulled in four hundred. What is my actual relationship to the ones I never named?
Do We Need a Package for This?

Four questions decide it: how much it does for you, how hard it would be to replace, what it drags in, and who maintains it.

Q · Should this be a dependency, code we write, or code we copy — and how do I answer that in under ten minutes?
API Stability

An interface with no external users still has consumers. Changing it frequently across many of them costs coordination, and that cost is invisible in every individual diff.

Q · This module is internal and we can change it whenever we like — so why does changing it keep costing us a week?
Semantic Versioning

A communication convention, not a guarantee. The number is a claim made by a human about their own code, and humans get it wrong in both directions.

Q · If a patch release cannot break me, why did a patch release break me?
Deprecation

Introduce the new thing, migrate the consumers, warn on the old, then remove it. Skipping a step does not save time; it moves the cost onto whoever is unlucky.

Q · How do I get rid of an internal interface that other people's code still calls?

Repository & Team Structure

7 lessons

How a repository is laid out, monorepo against polyrepo, ownership without silos, and the design reviews and RFCs that stop decisions living in one head.

Repository Structure

A folder tree is a navigation aid and a change-locality claim. It should reflect how engineers actually move through the system, not a template someone copied.

Q · How should the top-level folders be laid out, and why is every answer someone gives me a template from a different project?
Monorepo vs Polyrepo

Atomic changes, tooling, ownership, build scale, access control and release independence — six axes that pull in different directions. There is no universal answer, and anyone who gives you one is describing their last job.

Q · One repository or many, and which of the arguments people make about this actually apply to us?
Code Ownership

Somebody has to be accountable for each part of the system, and the mechanism that achieves that is the same mechanism that creates silos. The tension is real; pretending it is not is how both failures happen.

Q · How do I get clear accountability for every part of the codebase without producing a team nobody else can change code around?
Bus Factor

Critical knowledge living in one head is a design risk, not an HR risk. It shows up in the estimate for every change to that area, long before anyone leaves.

Q · How many people would have to be unavailable before we could not safely change this part of the system, and what does the answer cost us today?
Knowledge Sharing

Code review, documentation, pairing, design review and postmortems each spread a different kind of knowledge at a different cost. Choosing between them starts with naming which kind is missing.

Q · Which knowledge is failing to spread here, and which mechanism actually spreads that kind?
Design Review
▶ lab

Before a high-impact change, discuss requirements, options, trade-offs, migration, failure modes, security and observability — because this is the last point at which a boundary can still be moved.

Q · When is the right moment to review a design, and what has to be on the table for the review to be worth an hour of several people?
RFCs

Problem, Goals, Non-Goals, Design, Alternatives, Risks, Rollout, Open Questions. A written proposal for decisions too large for a meeting — and a reliable source of theatre when applied to decisions that are not.

Q · When is a written proposal worth more than a conversation, and how do I tell whether our RFC process is producing decisions or documents?

Designing Agentic Systems

5 lessons

Treating a model as an external dependency that is non-deterministic, fallible and costly — and keeping the invariants in code the moment they have to hold.

Designing a System That Has a Model In It

The design question is not how to prompt. It is which decisions you are delegating to a component that will answer differently tomorrow, and which ones you are keeping in code.

Q · Which decisions in this feature am I handing to a non-deterministic component, and which ones must stay deterministic?
The Model Is a Dependency

Non-deterministic, fallible, mutable under you, priced per call and slow. Four of those five you already know how to design around; the fifth is the only genuinely new thing.

Q · What kind of dependency is a language model, and which of my existing techniques still apply to it?
Business Logic Hiding in a Prompt

A rule that exists only in prompt text is hard to test, hard to enforce, hard to audit, and changes silently when someone edits a sentence or upgrades a model.

Q · This rule lives in the prompt. Is that a reasonable place for it, or is it logic that escaped the codebase?
Designing a Tool Interface

An API whose caller will not read the documentation carefully, will pass malformed arguments, and will invent plausible parameters that do not exist. Design for that caller.

Q · What does an interface look like when the caller is capable, confident, and unable to be held to a contract?
Where the Probabilistic System Ends

Four boundaries — workflow, permission, output and fallback — and the argument that each one must be a thing in the code rather than an understanding in someone's head.

Q · Where exactly does my deterministic system stop and the probabilistic one begin, and can a new engineer see that line without being told?