A Practical Primer · Software Engineering
The decisions you make early, that are expensive to change later — and how to make them well.
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?"
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.
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.
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.
These are the load-bearing ideas. Almost every good architectural decision is one of these applied in context.
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.
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.
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.
Build from replaceable parts with clear boundaries. A module you can understand, test, and deploy in isolation is a module you can reason about.
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.
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.
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.
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.
| Attribute | The question it answers |
|---|---|
| Scalability | Can it handle 10× the load by adding resources? |
| Availability | What fraction of the time is it actually working? |
| Reliability | Does it produce correct results, and recover from faults? |
| Performance | How fast does it respond, and at what throughput? |
| Security | Can it protect data and resist misuse? |
| Maintainability | How cheaply can we change and extend it? |
| Testability | Can we verify behavior in isolation? |
| Observability | Can we tell what it's doing in production? |
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.
Styles are reusable solution shapes. You'll often combine several. None is "best" — each trades complexity for some quality attribute.
Organize code into horizontal layers, each only calling the one below it.
Use when: most business apps; the safe, well-understood default.
One codebase, one deployment. Simple to build, test, and reason about early on.
Use when: starting out, small teams. Start here.
Small services, each owning its data, deployed independently, talking over the network.
Use when: large orgs needing independent scaling & release. Costly early.
Components react to events asynchronously instead of calling each other directly.
Use when: high decoupling, real-time flows, spiky load.
Clients request, a server provides. The backbone of the web.
Use when: almost any networked application.
Pure domain core surrounded by adapters for the DB, UI, and external systems.
Use when: you want the core insulated and testable.
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.
These touch every part of the system and must be designed once, centrally — not bolted on per-feature.
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?"
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.
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.
An architecture that lives only in one person's head is a liability. Two lightweight, high-leverage tools:
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.
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?"
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.
Architecture is iterative, not a one-time upfront phase. A workable loop:
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.
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.
CLAUDE.md / AGENTS.md / rules files) are the agent's brain.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.
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.
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.)
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.
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.