Software Engineering & Design

How do I turn changing requirements into software that is understandable, testable, maintainable, evolvable and safe to change over time? Not a SOLID recital, not a pattern catalogue, not clean-code slogans.

The question this domain answers

Good software design controls the cost of change.

That is the whole claim, and it is deliberately falsifiable. Not elegance, not pattern count, not how it reads in a screenshot — the measurable thing a design decides is what the next requirement costs: how many modules move, how many tests move, whether anything has to be migrated, and whether one person can still reason about the result locally. A learner who finishes this hears “add one more feature” and does not reach for a pattern. They ask what changed, which invariant moved, who owns it, and what the change after this one now costs.

The design loop every lesson carries
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

The order is the argument. The requirement and its constraints decide which invariants must hold; the invariants decide where responsibility for protecting them sits, and therefore where the boundaries fall; the boundaries decide what an interface has to say, and therefore what can be tested in isolation. Start in the middle — pick a pattern, draw layers, add a container — and you get a structure that is locally defensible and expensive to change, because nothing in it was derived from something that has to be true.

This is the codebase domain

Software Engineering & Design does not own every layer it touches. It owns the code a team edits every day — and deep-links its neighbours rather than re-teaching them.

Upstream

Software Architecture decides how subsystems are bounded and in what style, and System Design decides how a whole distributed system meets its scale and reliability goals. Both hand down constraints this domain has to live inside — and both leave open the question of how the code *inside* a subsystem is structured so it can change.

Here

The codebase a team edits daily: responsibilities, invariants, interfaces, state, dependencies, changeability. Which of our internal seams deserve to be a contract at all, who owns which data, what testability is telling us about the design, and what the next change costs. No other domain answers that last one.

Downstream

API Design, Backend, Frontend, Database and Security Engineering each own a layer in full, and Testing & Reliability owns confidence and resilience. Lessons here teach the structural decision and hand the rest off by name.

This domain's advice is contested more than any other here. SOLID, Clean Architecture, DDD and “never duplicate” are not settled findings — competent, experienced engineers genuinely disagree about all of them, and presenting a disputed position as consensus is the characteristic way a design curriculum goes wrong. So every claim carries a scope label saying what it is specific to — the paradigm, the language, the team size, the domain complexity, how long the code must live — and where a different context would flip it. Where a lesson is labelled CONTESTED, it states the strongest form of the opposing view, not a caricature of it. If it cannot, it does not understand the argument well enough to teach it.

Flagship experiences

The parts of this domain that are not reading.

Change Impact Explorer →

Pick a requirement change and see which modules, tests and deployments move — under a god-object design and a bounded one, side by side. The model is honest: a good design does not make every change cheap, it makes the *anticipated* ones cheap.

Follow a Requirement →

"Add Pause Subscription" from requirement through domain rule, state machine, contract, application logic, persistence, events, tests, migration and observability. Every step has a "go one layer deeper".

Responsibility Map →

Pick a unit; see what it knows, what it does, what it depends on, and every distinct reason it has to change. More than one or two reasons is the finding.

State Machine Lab →

Build order, payment and deployment lifecycles — then the lab checks which transitions you left reachable that should have been impossible to express.

Refactoring Lab →

Messy code, a menu of moves, and the coupling consequence of each — including the moves that make it worse while looking like progress.

Legacy Lab →

Change billing in a decade-old service with no tests: observe, characterize, find the seam, refactor, change, verify. Legacy means unprotected, not old.

Feature Design Lab →

States, rules, contract, persistence, errors, migration and tests defined *before* the first line of implementation — and what fails when each field is skipped.

Design Review Simulator →

A proposed design. Find the unclear ownership, the hidden state, the unsafe migration, the abstraction nobody needed and the failure nobody handled.

How Should I Structure This Code? →

A decision tree, plus refactor versus rewrite. Every leaf names why, what it costs, the simpler thing to try first, and how the recommended design itself fails.

Capstone: evolvable billing, legacy modernization →

Design a SaaS billing system, then absorb change requests one at a time — and modernize a system whose behaviour nobody can fully state.

Practice challenges →

Four levels, with the cause unlabelled. Each one carries the trap: the fix that looks right, passes review, and leaves the real design problem in place.

Interview guide →

What each question is really testing, what a strong answer sounds like, and the red flags that separate a recited acronym from a working model.

Learning modules

Thirty-eight modules, from what actually makes software hard to change to designing systems whose most unpredictable dependency is a model.

250 lessons →
Engineering Fundamentals7

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.

Requirements6

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.

Invariants5

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

Problem Decomposition7

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".

Abstraction6

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

Encapsulation & Information Hiding5

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

Cohesion & Coupling7

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.

Dependency Design7

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.

SOLID, Critically7

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

Composition & Inheritance5

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

Design Patterns11

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.

Domain Modeling10

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.

State & State Machines7

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

Error Modeling6

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.

Side Effects & Immutability7

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

Naming & Function Design6

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

Documentation5

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

Code Review6

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.

Refactoring9

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

Code Smells9

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.

Technical Debt5

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

Legacy Code7

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.

Migration & Compatibility6

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.

Evolvability7

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

Module & Package Structure8

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.

Architecture Boundaries6

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

Monolith & Modular Monolith5

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 Design5

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

Debuggability by Design6

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

Testing as Design Feedback7

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.

Designing for Failure7

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 Security5

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

Designing for Cost5

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.

Complexity7

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

Engineering Decisions8

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

Dependency Management6

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

Repository & Team Structure7

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.

Designing Agentic Systems5

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.

Reference

For when you already know roughly what you are looking for.

Don't delegate understanding

The point of the domain, stated plainly.

Frameworks
hide lifecycle
Patterns
hide trade-offs
ORMs
hide persistence
Dependency injection
hides wiring
Architecture templates
hide assumptions
AI coding tools
hide design decisions

Use all of them. Every one is worth using, and rebuilding them by hand is not the lesson. But know what owns the state, what protects the invariant, what depends on what and in which direction, and how the system changes tomorrow — because when one of these abstractions leaks, and each leaks in a characteristic way, you want to be debugging a system you have a model of rather than one you have been trusting.