Principles
Small, sharp rules that hold up across languages and eras, followed by the type-driven techniques that let the compiler enforce them.
Core principles
Separation of concerns, high cohesion, low coupling
Things that change together live together; things that do not, do not know each other.
- Watch out
- The root of most of the other principles in this canon.
SOLID
Five object-oriented design principles; the two with the widest reach are single responsibility (one reason to change per module) and dependency inversion (depend on abstractions you own).
- Watch out
- Dependency inversion is the mechanism behind hexagonal and clean architecture.
- Origin
- Robert C. Martin
- Serves
- Hexagonal architecture (ports and adapters) (Dependency inversion is the mechanism.), Clean and onion architecture (Dependency inversion is the mechanism.)
Composition over inheritance
Assemble behaviour from parts instead of inheriting it.
Fail fast
Reject bad input at the boundary, loudly.
Principle of least astonishment
Behaviour matches what the name promises.
DRY
Every piece of knowledge has one authoritative home.
- Also known as
- Don’t repeat yourself
- Watch out
- It is about knowledge, not textual similarity. A wrong abstraction costs more than duplication, so wait for the rule of three.
- Origin
- Andy Hunt and Dave Thomas, 1999
KISS
The simplest thing that satisfies the actual requirement.
- Also known as
- Keep it simple, stupid
- Watch out
- Simple is not the same as easy. Measure simplicity by the number of concepts a reader must hold.
YAGNI
Do not build for speculative futures.
- Also known as
- You aren’t gonna need it
- Watch out
- It does not forbid clean seams: a port is a cheap boundary, not speculation.
Law of Demeter, tell don’t ask
Talk to your neighbours, not to their internals.
- Also known as
- Principle of least knowledge
- Origin
- Ian Holland, 1987
- Sources
- Law of Demeter (General Formulation)
Type-driven design
Make illegal states unrepresentable
Model the domain so that invalid combinations cannot be expressed. Prefer sum types over flag soup.
- Watch out
- Not every constraint fits the type system; when one does not, fall back to a smart constructor.
- Origin
- Yaron Minsky
- Served by
- Parse, don’t validate (The boundary-facing, dynamic side of the same idea.), Newtypes and typed primitives, Typestate
- Sources
- Make illegal states unrepresentable
Parse, don’t validate
Turn untrusted input into a precise type once, at the boundary; downstream code never re-checks.
- Watch out
- Validation throws away its proof; parsing keeps it in the type. Where the type system cannot express a constraint, use an abstract newtype with a smart constructor and use judgement.
- Origin
- Alexis King, 2019
- Serves
- Make illegal states unrepresentable (The boundary-facing, dynamic side of the same idea.)
- Sources
- Parse, don’t validate
Newtypes and typed primitives
Use Email, UserId or Cents instead of raw string and int. Smart constructors are the only way in.
- Watch out
- The cure for primitive obsession.
- Serves
- Make illegal states unrepresentable
- Sources
- Make illegal states unrepresentable
Typestate
Encode state machines in types: transitions are functions between distinct types, for example Draft to Submitted.
- Serves
- Make illegal states unrepresentable
Total functions and explicit errors
Use Result and Option; keep hidden exceptions and nulls out of the domain.
Immutability by default
Data is immutable unless there is a reason; invariants are encapsulated behind private constructors.