Architecture isn't diagrams for their own sake. It's the small set of decisions — how the system is split, where state lives, what talks to what — that are cheap to make on day one and ruinous to undo a year in. The skill is making those decisions deliberately, recording why, and being honest about the trade-offs. This is the discipline every 2nth build draws on: ADRs, the C4 model, trade-off matrices, a working catalogue of patterns, and the judgement to know when to break them.
A useful working definition, after Ralph Johnson and Martin Fowler: architecture is the things that are hard to change — and the shared understanding of why they are the way they are. Renaming a function is not architecture. Choosing a single database that every service shares, or splitting a system into twelve deployable pieces, is. The first you can undo in an afternoon; the second you live with for years.
That framing does real work. It tells you where to spend care: not on every line, but on the handful of decisions whose cost of being wrong is high and whose cost of reversal is higher. It also tells you the deliverable. Architecture's output isn't a pretty diagram — it's a decision, its alternatives, and the reasoning, written down where the next person (or the next agent) can find it.
The most common failure isn't picking the wrong pattern. It's making the decision implicitly — nobody chose the distributed monolith; it accreted — and leaving no record of why, so every future change re-litigates settled ground or breaks an invariant no one remembered was load-bearing.
You don't need a heavyweight framework. Three lightweight artefacts — a decision record, a model that draws the system at the right zoom level, and a matrix that makes a trade-off explicit — cover the vast majority of real architectural work and travel well between humans and agents.
ADRs — Architecture Decision Records. A short markdown file per significant decision, in the repo next to the code. Michael Nygard's original template is four headings: Context (the forces at play), Decision (what we chose), Status (proposed / accepted / superseded), Consequences (what becomes easier and harder). Numbered, append-only, never deleted — a superseded ADR stays as the record of what we believed at the time.
# ADR-014: Use a modular monolith, not microservices Status: Accepted Context: 4-person team, one product, deploys daily. Microservices would add network failure modes and ops overhead we can't staff. Decision: One deployable, enforced module boundaries, one database with per-module schemas. Revisit if a module needs independent scaling. Consequences: + Simple deploys, in-process calls, easy local dev + Boundaries already drawn if we ever split out a service − Shared database is a coupling risk — needs discipline
The C4 model (Simon Brown) draws the system at four zoom levels, so a diagram answers exactly one question instead of becoming an everything-map nobody trusts:
The system as one box, and the people and external systems around it. The diagram for non-technical stakeholders.
The deployable units — web app, API, database, queue. The single most useful level for most conversations.
The major building blocks inside one container and their responsibilities. Drawn only where it earns its keep.
Classes / functions. Usually best left to the IDE — rarely worth hand-drawing or maintaining.
Trade-off matrices turn an argument into a table. List the options down the side, the qualities that matter across the top (operability, cost, latency, team fit, blast radius), and fill the cells honestly — including the boxes where your preferred option loses. A decision that survives its own trade-off matrix is defensible; one that can't be written as a matrix usually isn't a decision yet, it's a preference.
A pattern is a named, reusable shape with a known set of trade-offs. Knowing them by name is leverage: it turns a vague debate into "we're choosing between a modular monolith and event-driven services, and here's why." None is universally right — each is the answer to a specific pressure.
One deployable, one codebase. Simplest to build, test and reason about.
Use: most new products, small teams.A monolith with enforced internal boundaries. Simplicity now, clean seams for later.
Use: the default first move in 2026.Many small independently-deployable services. Independent scaling & teams — at real ops cost.
Use: many teams, proven scale need.Components react to events on a bus rather than calling each other directly. Loose coupling, async resilience.
Use: workflows, fan-out, decoupling.Separate the write model from the read model. Optimise each independently.
Use: heavy reads, complex reporting.Store the sequence of changes, not just current state. Full audit, time-travel — and complexity.
Use: audit-critical, finance, ledgers.Ports & adapters: domain logic at the centre, I/O at the edges behind interfaces.
Use: long-lived, testable cores.Presentation → application → domain → data. The familiar default; risks anaemic domains.
Use: CRUD-heavy line-of-business apps.Functions that scale to zero; no servers to manage. Pay-per-use, cold-start trade-offs.
Use: spiky, event-shaped workloads.Run compute close to the user (Workers, CDN edge). Low latency, constrained runtime.
Use: global latency-sensitive apps.Wrap a legacy system and replace it route-by-route until the old one withers.
Use: replacing a system you can't stop.Backend-for-frontend per client; sagas coordinate multi-service transactions without a global lock.
Use: multi-client UIs, distributed writes.For most new products with a small team, start with a modular monolith and reach for an edge-first or serverless deployment surface. You get simplicity now and clean module boundaries that make extracting a service later a refactor, not a rewrite. Microservices are a response to organisational scale (many teams shipping independently) far more often than technical scale — adopting them before you have that problem buys you distributed-systems pain with none of the payoff.
An anti-pattern is a recurring "solution" that creates more problems than it solves. They're worth naming because they're rarely chosen — they accrete — and a name is what lets a team say "that's a distributed monolith" before it's load-bearing.
| Anti-pattern | How you spot it | The fix direction |
|---|---|---|
| Distributed monolith | Microservices that must deploy together and share a database | Merge back, or sever the shared data and the lock-step releases |
| Big ball of mud | No discernible structure; everything reaches into everything | Introduce boundaries incrementally; strangler-fig the worst parts |
| Golden hammer | One tech (k8s, GraphQL, microservices) applied to every problem | Match the tool to the pressure; matrix the alternatives |
| Premature microservices | Twelve services, one team, weekly cross-service changes | Collapse to a modular monolith until scale forces a split |
| Anaemic domain | Objects with data but no behaviour; all logic in "services" | Move invariants onto the domain; thin the service layer |
| Chatty integration | N+1 network calls to render one screen | Batch, a BFF aggregation layer, or denormalise the read model |
Almost every anti-pattern is a coupling problem wearing a decoupling costume. The distributed monolith bought network calls and got tighter coupling. Premature microservices bought independence and got lock-step deploys. When something feels harder than it should, look for the place two things that should move independently are quietly chained together — that's usually the root, and the diagram rarely shows it.
Architecture is a cost, paid in time and constraint. Like any cost it's worth paying only where the return is real. The skill includes knowing when not to architect — and when a clean rule should be broken on purpose, with the reason written down.
Good architecture isn't dogma — it's owning the trade-off out loud. Shipping a shared database across two services to hit a deadline can be the right call. The discipline is to write the ADR that says you did it, why, and what would trigger undoing it. A documented, deliberate violation is engineering. The same shortcut taken silently is the first plank of the next big ball of mud. The rule isn't "never break the rule"; it's "never break it without leaving a note."
SA delivery sharpens the trade-offs. Intermittent power and connectivity, a weak currency against USD-billed cloud, POPIA data-residency questions, and small teams all push architecture toward resilience, frugality and clear ownership — not theoretical purity.
Designs that assume always-on infrastructure fail badly during load-shedding and flaky links. Favour queue-and-retry over synchronous chains, offline-tolerant clients, and edge-first deployment that keeps working when the origin is unreachable. Event-driven shapes aren't an academic preference here — they're how the system survives a 2-hour outage without losing work.
Every USD-billed service is a rand risk. Architecture is where you control it: scale-to-zero serverless for spiky workloads, ruthless attention to egress (the hidden tax of multi-cloud), and a bias toward owned, fixed-cost infrastructure where volume justifies it. The cheapest architecture is often the one with the fewest moving parts billed by the hour.
POPIA makes "where does each piece of data live, and who can reach it?" an architectural question, not a legal afterthought. A clear container diagram that marks data stores by region — af-south-1 / South Africa North for residency-sensitive data, elsewhere for the rest — is half of a compliance conversation done. Draw the boundary before the auditor asks you to.
The canonical references behind this leaf — the C4 model, the ADR pattern, and the standard texts on architectural trade-offs. Last reviewed 2026-06-17.