← Back to Autonomy

A Practical Primer · Software Engineering

System Architecture Design

The decisions you make early, that are expensive to change later — and how to make them well.

§ 01

What architecture actually is

Software architecture is the set of structures needed to reason about a system — the components, what they do, how they relate, and the principles governing their design and evolution.

A useful working definition: architecture is made up of the decisions that are hard to change later. Choosing a button color is not architecture. Choosing whether your system is a single deployable unit or fifty independent services is — because reversing it costs months.

It sits one level above code. Code answers "how does this function work?" Architecture answers "what are the major pieces, who owns what, how do they talk, and what happens when one fails?"

Practical note

A good test for whether something is "architectural": ask "If we got this wrong, how painful is the fix six months from now?" The more painful, the more deliberate your decision should be — and the more it deserves to be written down.

§ 02

Why it matters

Every system has an architecture whether or not anyone designed it on purpose. The question is only whether it was intentional or accidental. Accidental architecture is the slow accumulation of expedient choices that eventually make every new feature harder than the last.

Conway's Law

Organizations design systems that mirror their own communication structure. If you have four teams building a compiler, you'll get a four-pass compiler. Design your team boundaries and your component boundaries together — they're the same problem.

§ 03

Core design principles

These are the load-bearing ideas. Almost every good architectural decision is one of these applied in context.

Separation of concerns

Each part of the system should address one distinct responsibility. Keep business logic out of the UI, keep data access out of business logic. When concerns are tangled, a change to one forces changes to the others.

High cohesion, low coupling

Cohesion = things that change together live together. Coupling = how much one module must know about another. The goal is always the same: maximize cohesion within a module, minimize coupling between modules. This single sentence explains most of microservices, layering, and clean architecture.

Abstraction & encapsulation

Expose what a component does through a stable interface; hide how it does it. Callers depend on the contract, not the implementation, so the implementation can change freely.

Modularity

Build from replaceable parts with clear boundaries. A module you can understand, test, and deploy in isolation is a module you can reason about.

SOLID — at architectural scale

The SOLID principles scale up from classes to whole components. The most architecturally important is Dependency Inversion: high-level policy should not depend on low-level detail; both depend on abstractions. This is what lets you swap a database, a message broker, or an external API without rewriting your core logic.

Don't over-engineer

KISS (keep it simple), YAGNI (you aren't gonna need it), and DRY (don't repeat yourself) are guardrails against complexity. The most common failure of inexperienced architects is not too little structure — it's too much, added too early, for problems they don't yet have.

Caution

DRY is about knowledge, not text. Two pieces of code that look identical but represent different business rules should stay separate — merging them couples concepts that will diverge. Premature DRY creates the worst coupling: invisible coupling between unrelated features.

§ 04

Quality attributes — the "-ilities"

Functional requirements say what the system does. Quality attributes (non-functional requirements) say how well — and these are what architecture exists to deliver. You cannot maximize all of them; you choose which matter for this system.

AttributeThe question it answers
ScalabilityCan it handle 10× the load by adding resources?
AvailabilityWhat fraction of the time is it actually working?
ReliabilityDoes it produce correct results, and recover from faults?
PerformanceHow fast does it respond, and at what throughput?
SecurityCan it protect data and resist misuse?
MaintainabilityHow cheaply can we change and extend it?
TestabilityCan we verify behavior in isolation?
ObservabilityCan we tell what it's doing in production?
Practical note

Make quality attributes measurable before you design for them. "Fast" is useless; "95th-percentile response under 200 ms at 1,000 requests/sec" is a target you can architect toward and test against. These concrete statements are called quality attribute scenarios.

§ 05

Common architectural styles

Styles are reusable solution shapes. You'll often combine several. None is "best" — each trades complexity for some quality attribute.

Layered (n-tier)

Presentation · Logic · Data

Organize code into horizontal layers, each only calling the one below it.

Use when: most business apps; the safe, well-understood default.

Monolith

Single deployable

One codebase, one deployment. Simple to build, test, and reason about early on.

Use when: starting out, small teams. Start here.

Microservices

Independent services

Small services, each owning its data, deployed independently, talking over the network.

Use when: large orgs needing independent scaling & release. Costly early.

Event-driven

Producers · Brokers · Consumers

Components react to events asynchronously instead of calling each other directly.

Use when: high decoupling, real-time flows, spiky load.

Client–Server

Request · Response

Clients request, a server provides. The backbone of the web.

Use when: almost any networked application.

Hexagonal / Ports & Adapters

Core · Adapters

Pure domain core surrounded by adapters for the DB, UI, and external systems.

Use when: you want the core insulated and testable.

CLIENT ──HTTP──▶ API GATEWAY ──┬──▶ Service A ──▶ DB A ├──▶ Service B ──▶ DB B └──▶ Service C ──▶ DB C │ ▼ EVENT BUS (async, decoupled)
The microservices trap

Microservices solve organizational scaling problems by trading code complexity for operational complexity (networking, distributed transactions, monitoring, deployment). Most systems should begin as a well-structured modular monolith and extract services only when a real, measured pressure demands it. "We might need to scale" is not that pressure.

§ 06

Cross-cutting concerns

These touch every part of the system and must be designed once, centrally — not bolted on per-feature.

§ 07

Trade-offs & reality

There are no right answers in architecture — only trade-offs. The mature mindset is not "what's the best pattern?" but "what are we optimizing for, and what are we willing to give up?"

The CAP theorem

In a distributed system, when a network partition happens you must choose between Consistency (every read sees the latest write) and Availability (every request gets a response). You cannot have both during a partition — and partitions will happen. Banking leans consistent; a social feed leans available.

Practical note

Every architecture decision should name what it costs. Microservices buy independent deployment at the price of operational complexity. Caching buys speed at the price of staleness. Strong consistency buys correctness at the price of latency. Write the cost down next to the benefit — that record is worth more than the decision itself.

§ 08

Documenting the design

An architecture that lives only in one person's head is a liability. Two lightweight, high-leverage tools:

The C4 model

Diagram the system at four zoom levels, like a map: Context (system + users + external systems) → Containers (apps, services, databases) → Components (inside a container) → Code (rarely needed). Most conversations only need the first two levels.

Architecture Decision Records (ADRs)

A short markdown file per significant decision, capturing the context, the decision, and the consequences. Cheap to write, invaluable in eighteen months when someone asks "why on earth did we do it this way?"

Practical note

Diagrams document structure; ADRs document reasoning. You need both. The reasoning is what's actually scarce — anyone can redraw a box, but no one can recover the trade-off discussion that produced it.

§ 09

A practical process

Architecture is iterative, not a one-time upfront phase. A workable loop:

  1. Understand the drivers. Gather functional requirements, the key quality-attribute scenarios, and the real constraints (budget, team skills, deadlines, existing systems).
  2. Identify the hard parts. Find the decisions that are expensive to reverse and the highest technical risks. Spend your design energy here.
  3. Sketch candidate structures. Decompose into components; pick styles; define how they communicate. Produce two or three options, not one.
  4. Evaluate against the scenarios. Walk each candidate through your quality-attribute scenarios. Where does it strain? Make the trade-offs explicit.
  5. Decide and record. Choose, and write an ADR. The record matters as much as the choice.
  6. Validate early. Build a thin vertical slice or a spike to test the riskiest assumption before committing the whole team.
  7. Evolve. Revisit as you learn. Good architecture is designed to absorb change, not to predict it perfectly.
§ 10

Common mistakes

The throughline

Nearly every mistake above is a failure of matching the architecture to the actual problem — either too much structure for the problem at hand, or too little attention to the qualities that problem genuinely demands. Architecture is the discipline of fitting the solution to the real constraints, then writing down why.


§ 11

Architecting with coding agents

When an AI agent writes much of the code, architecture becomes more important, not less. The bottleneck moves from typing code to specifying intent and verifying output — and both of those are architectural acts.

A coding agent (Claude Code, Cursor, Copilot, and the like) is a fast, tireless, literal junior engineer with no long-term memory and an unjustified confidence. It will produce plausible code at speed. Your job shifts from author to architect, reviewer, and verifier. The principles in the earlier sections don't change — cohesion, coupling, clear boundaries, documented decisions — but they now do double duty: they're also what makes the agent effective, because a well-bounded codebase is one the agent can navigate without loading everything at once.

The mental shift

A procedure for agent-assisted architecture

  1. You own the architecture; the agent owns the typing. Make the structural and irreversible decisions yourself — the styles, the boundaries, the data ownership, the trade-offs. Delegate implementation within those boundaries, not the boundaries themselves.
  2. Write the ground truth first. Before generating code, put the architecture, conventions, tech stack, and constraints into files the agent reads on every task. This is the single highest-leverage thing you can do for output quality.
  3. Plan before code. Ask the agent to produce a plan or design — files it will touch, interfaces, approach — and review that before any implementation. Catching a wrong approach in a plan costs a sentence; catching it in a 600-line diff costs an afternoon.
  4. Fix the contracts, then fill them. Define types, function signatures, API schemas, and tests first. Let the agent implement against a frozen interface. Stable contracts keep its freedom productive instead of chaotic.
  5. Decompose into agent-sized tasks. Small, bounded, independently verifiable units — exactly the high-cohesion / low-coupling decomposition you'd want anyway. One clear task beats one sprawling instruction.
  6. Build the verification harness. Tests, type checks, linters, and CI are not optional here — they are how you contain "confidently wrong" output. The test suite is the spec the agent is effectively optimizing against, so make it real.
  7. Keep diffs small and review every one. Atomic, reviewable changes. Speed of generation is worthless if the change is too large to scrutinize. Trust nothing you haven't read or tested.
  8. Lock decisions in ADRs. Use the agent to explore options and generate spikes — then record the chosen decision and its reasoning so the next session (human or agent) doesn't relitigate it.
HUMAN decides architecture, contracts, trade-offs │ ▼ (spec + conventions files = ground truth) AGENT plans ──▶ you review plan ──▶ implements within bounds │ ▼ HARNESS tests · types · lint · CI ◀── the real guardrail │ ▼ HUMAN reviews small diff ──▶ merge ──▶ record ADR

Practical notes

Context is a budget

Agents degrade as their context fills with noise. Point them at the specific files that matter rather than the whole repo, keep tasks narrow, and start a fresh session when a thread gets long and muddled. A focused agent on three files beats a confused one on three hundred.

Confidently wrong

Generated code compiles and reads well and can still violate your intent, silently mishandle an edge case, or quietly break an invariant elsewhere. Treat every output as a pull request from a stranger: verify against tests and the actual requirement, never against how convincing it looks.

Security & dependencies

Review anything an agent pulls in. Watch for invented or unvetted packages, secrets pasted into code, missing input validation, and over-broad permissions. The agent optimizes for "make it work," not "make it safe" — that judgment stays with you. (Relevant when moving between tools and enterprise environments with their own export-control and compliance constraints.)

Consistency drift

Left alone, agents introduce subtly different patterns for the same problem across the codebase. Enforce style through formatters, linters, and a conventions file so consistency is mechanical rather than a thing you police by hand in review.

Where agents shine vs. where they don't

Strong: well-specified implementation, boilerplate, test generation, refactors within clear boundaries, exploring several options quickly, explaining unfamiliar code. Weak / keep human-led: cross-cutting architectural changes, novel trade-off decisions, anything where being subtly wrong is expensive and hard to detect.


§ 12

References & further reading