01/14What it is

LEARN TO PROGRAM · CHAPTER 13 · ARCHITECTURE

Software architecture

Small programs are a list of steps. Big ones are cities: many parts, built by many people over years, that must keep working while they change. Software architecture is the shape of that city: what the parts are, how they talk, and which decisions would be painful to undo.

All through this chapter you will build and rebuild one small online bookshop. In the 3D, its six parts are districts of a city. Scroll, and watch the same six districts become layers, a hexagon, a monolith, a set of services and an event-driven system.

Every code block runs in your browser. Edit it, break it, press Run.

01 · WHAT IT IS

What architecture is: the decisions that are hard to change

Look at the city. The towers in each district are being torn down and rebuilt all the time, and nothing else notices: that is ordinary code, cheap to change. The roads between districts, the district borders and the database in the middle are different. Move one of those and half the city has to move with it.

That is the most useful definition of architecture there is. Martin Fowler, after a long argument with Ralph Johnson, settled on "things that people perceive as hard to change", and Johnson's own version: architecture is about the important stuff, whatever that is. The textbook definition, from Bass, Clements and Kazman's Software Architecture in Practice, says the same thing more formally: the structures you need to reason about a system, meaning its parts, the relations between them, and the properties of both.

Here is the idea in numbers. The same app is written twice; count how many files must change to swap the database:

blast-radius.js

Six files against two. Try asking about "html" instead: what would a new UI framework cost in each version?

The second version made one decision (which database) cheap to change by hiding it behind a boundary. That is the job: decide which things must be hard to change, keep those few in good order, and make everything else easy.

02 · MODULES

Modules and boundaries

A module is a part of the program with a wall around it and a gate in the wall. The gate is its interface: the few functions others may call. Everything behind the wall is its implementation, which it may change at any time. In the 3D, Orders gets its wall; the web pages reach it only through placeOrder(), and everything Orders needs from other districts leaves through the same gate.

In 1972 David Parnas showed that the best way to split a system is not by the steps of the work (read input, process, write output) but by secrets: give each module one design decision that is likely to change, and hide it. He called it information hiding, and it is still the foundation of everything else in this chapter.

cart-module.js

The lines map lives in a closure: it doesn't exist as far as the outside world is concerned. Try adding remove(isbn) without changing how anyone calls the cart.

John Ousterhout's A Philosophy of Software Design adds a measure of a good module: it should be deep. A deep module has a small, simple interface over a lot of useful work; Unix file I/O hides disks, caches and permissions behind about five calls (open, read, write, lseek, close). A shallow module has an interface nearly as complicated as what it does, so it adds things to learn without hiding anything.

In JavaScript, files are modules: anything you don't export is private. Classes have #private fields. In Eric Evans' Domain-Driven Design, the biggest walls are bounded contexts: areas of the business where a word means one thing. "Order" in the shop (a basket being paid for) and "order" in the warehouse (a box being packed) can be two different models, each in its own context.

03 · COUPLING & COHESION

Coupling and cohesion

Two words, coined by Stevens, Myers and Constantine in 1974, describe how good a set of boundaries is:

  • Coupling is how much modules depend on each other. With high coupling, a change in one ripples into many. You want it low.
  • Cohesion is how much the things inside one module belong together. A module called utils holding date formatting, tax rules and email templates has low cohesion. You want it high.

The 3D cycles between two versions of the same shop. In the tangled one, every district talks to every other, and each district's towers are a jumble of colours: code for one job scattered everywhere. In the clean one there are six roads, and each district is one colour. Same features, very different cost of change.

You can measure coupling. For each module, count how many modules use it (fan-in) and how many it uses (fan-out). Robert C. Martin's instability is fan-out ÷ (fan-in + fan-out): 0 means everybody leans on you, so you had better be stable; 1 means nobody depends on you, so you are free to change.

coupling.js

Orders scores 1.00: it uses four modules and nothing uses it, which is right for the top of the app. Add "orders" to accounts' list and run it: you've just made a cycle (orders → accounts → orders).

The healthy direction: depend on things that are more stable than you. If the stable heart of your system depends on a volatile detail, every change to the detail shakes the heart.

04 · LAYERS

Layers: presentation, domain, data

The oldest architecture pattern slices a program horizontally. In the 3D, every district becomes a building of three floors:

  • Presentation (top): web pages, an API, a command line. Turns requests into calls and results into something a person or program can read.
  • Domain (middle): the rules of the business. "You can't buy more copies than are in stock." No HTML, no SQL.
  • Data (bottom): storing and loading. Databases, files, caches.

The rule that makes it work: calls go down, never up. Presentation may call domain, domain may call data. The red arc in the 3D is a data-layer module calling up into another district's presentation: that is the kind of shortcut that, repeated a hundred times, turns layers back into a tangle.

layers.js

Press Step through and watch the call stack: handle → buy → find, down through the floors and back up. Then change the stock rule: only the domain layer changes.

Look at the 3D again: layers are horizontal slices, modules (the districts) are vertical ones. Good systems usually do both, and there are two ways to arrange the files:

two ways to arrange the same code
src/                      src/
  controllers/               catalog/
    catalog.js                 catalog.page.js
    orders.js                  catalog.rules.js
  services/                    catalog.store.js
    catalog.js               orders/
    orders.js                  orders.page.js
  repositories/                orders.rules.js
    catalog.js                 orders.store.js
    orders.js

  "package by layer"         "package by feature"

Package by feature keeps each module's three layers together, so a change to "orders" touches one folder. Package by layer is fine for small apps, but as they grow, every feature smears across every folder.

05 · PORTS & ADAPTERS

Ports and adapters: the hexagon

Layers still leave the domain sitting on top of the database. In 2005 Alistair Cockburn proposed something more symmetrical, the hexagonal architecture, also called ports and adapters. Put the domain in the middle. Everything else (the web, the command line, tests, the database, email, the card processor) is outside, and it all plugs in the same way:

  • A port is an interface the core owns: "I need something that can save orders", or "you can ask me to place an order".
  • An adapter connects one real technology to one port. Driving adapters (left) call into the core: HTTP, CLI, tests. Driven adapters (right) are called by it: PostgreSQL, email, payments.

Watch the database adapter on the right: every few seconds it slides out and an in-memory one slides in. The hexagon doesn't notice, because it only knows the port. The six sides mean nothing in particular; Cockburn just wanted room to draw several ports.

hexagon.js

The same makePlaceOrder core runs twice: once wired to a SQL adapter, once to an in-memory one for a test. Change the payments rule to amount < 50 and see which calls are declined.

The payoff is testing and change. The rules of the shop can be tested in milliseconds with no database, and a new way in (a mobile app, a message queue) is a new adapter, not a rewrite. Robert C. Martin's Clean Architecture and the onion architecture are the same idea drawn as rings.

06 · DEPENDENCY INVERSION

Dependency inversion: flipping the arrow

Something subtle happened in the hexagon. At run time the core still calls the database (to save an order). But in the source code, the database adapter depends on the core, because it implements an interface the core defined. The arrow of dependency points the opposite way to the flow of control. In the 3D, the red arrow is the naive version (the core imports the database); watch it flip into the green one (the adapter plugs into the core's port).

This is the dependency inversion principle, the D in Robert C. Martin's SOLID: high-level modules should not depend on low-level modules; both should depend on abstractions, and abstractions should not depend on details.

dependency-inversion.js

The "before" class can't even be tested without a database server. The "after" one takes any object with a save() method.

The usual way to do it is dependency injection: pass a module what it needs (as constructor or function arguments) instead of letting it build or import its own. Somewhere at the very edge of the program, often a file called main, one piece of code creates the real adapters and wires everything together. That is the only place that knows about PostgreSQL. Fowler's 2004 article on injection is still the clearest explanation.

order-repository.ts · TypeScript
// domain/order-repository.ts: the core owns the interface
export interface OrderRepository {
  nextId(): Promise<number>;
  save(order: Order): Promise<void>;
}
 
// infra/postgres-orders.ts: the detail depends on the core
import type { OrderRepository } from "../domain/order-repository";
export class PostgresOrders implements OrderRepository { /* ... */ }

07 · MONOLITH

The same app as a monolith

Now the big decision: how many programs is your app? The districts pack together into a single block. A monolith is one deployable program with one database. Every module calls the others with ordinary function calls; one build, one deploy, one thing to monitor.

That is not an insult. A function call takes nanoseconds and can't time out. A transaction across orders and payments is one database transaction. You can rename a function across the whole app in one commit. Some of the busiest sites on the web run on monoliths.

The danger is not the single deployable; it is the big ball of mud. With nothing stopping it, any code can call any other code and read any table, and after a few years every change touches everything. The block in the 3D has no walls inside it, and that is the problem.

08 · MODULAR MONOLITH

The modular monolith

Same block, now with walls: each module has a boundary, a gate (its public API) and its own schema in the shared database, and nothing reads another module's tables. This is a modular monolith: one program to deploy and run, but the internal structure of a set of services.

modules.js

Orders didn't declare catalog, so it doesn't get it. Make catalog need "orders" and run it: the loader reports the cycle instead of letting it in.

In real codebases, the walls are enforced by tools: package visibility in Java or C#, separate packages in a monorepo, or lint rules that fail the build when one module imports another's internals. Shopify, one of the largest Ruby on Rails codebases in the world, chose exactly this in 2019 instead of microservices, and built an in-house tool (Wedge) to track how isolated each component had become.

The modular monolith is the sweet spot for most teams: the walls give you most of the maintainability benefits of services, and if one module really does need to scale or ship on its own one day, it is already separated and can be lifted out.

09 · MICROSERVICES

Microservices, with honest trade-offs

Now the districts fly apart. In microservices, each module is its own program, deployed on its own, with its own database that no one else may touch. Modules talk over the network, usually HTTP or messages. James Lewis and Martin Fowler's 2014 article set out the style: services organised around business capabilities, independently deployable, owned by small teams.

What you gain is real: a team can ship its service ten times a day without coordinating with anyone; a busy service can be scaled alone; one service can use a different language or database; a crash in recommendations need not take down checkout. On the cluster page you can watch services like these spread over many machines.

What you pay is also real. Every arrow in the 3D is now a network hop. Watch the call from orders to shipping: now and then it turns red. Run the numbers:

network-cost.js

Five function calls cost about half a microsecond; five round trips cost milliseconds. And if a checkout needs six services up at once, their downtimes add up: about 52 hours a year instead of 9.

Real systems fight back with caches, async messages and graceful degradation, so the uptime is not always that bad. But every remote call now needs a plan for failure:

retries.js

Order 2 failed twice and gave up, so the page has to cope. Retries also have a cost: if a service is overloaded, every client retrying makes it worse.

140+services that the data company Segment merged back into a single service in 2018, after the operational load of running them (deploys, on-call, shared libraries drifting apart) outgrew the benefits. Segment, Goodbye Microservices

The honest summary: microservices solve organisational scaling (many teams shipping independently) at the price of distributed-systems problems: partial failure, network latency, no cross-service transactions, harder debugging. Fowler calls this the microservice premium: worth paying only when the system is complex enough to need it. The famous fallacies of distributed computing ("the network is reliable", "latency is zero"...) are a list of the ways people forget to pay it.

10 · EVENTS & CQRS

Events, and CQRS briefly

So far, modules call each other: orders calls shipping, then email, then stats. Each new feature means changing orders. In an event-driven system, orders just announces a fact (OrderPlaced) and anyone interested listens. In the 3D the pink ring is an event bus; each event travelling around it lights up the districts that subscribe.

event-bus.js

Add a subscriber for "OrderCancelled" and publish one. Notice that placeOrder never changed when loyalty points were added.

The gain is decoupling: publishers don't know their subscribers, and a slow email service no longer slows checkout. The cost is that the flow of the program is no longer written down in one place. "What happens when an order is placed?" becomes a search across every subscriber. Fowler points out that "event-driven" covers several different patterns (notifications, state transfer, event sourcing), and it pays to know which one you mean.

CQRS (command query responsibility segregation) goes one step further: use one model to change data and a different one to read it. Commands go to the write model, which checks the rules; the events it produces feed one or more read models, each shaped for one screen. In the 3D, the gold stack in the centre is a read model, growing one row for each event that reaches it.

cqrs.js

This one also uses event sourcing: the events are the record, and the read model is rebuilt from them. The two ideas often travel together but are separate.

11 · PATTERNS

Patterns you will meet everywhere

Inside the modules, the same small designs keep coming back. The 1994 book Design Patterns (Gamma, Helm, Johnson and Vlissides, the "Gang of Four") catalogued 23 of them; Fowler's Patterns of Enterprise Application Architecture (2002) added the ones for business apps. Four you have already been using in this chapter, each running in the 3D as a little machine:

Repository: domain code asks for objects as if from an in-memory collection, and the repository hides the storage behind it. (The purple gatehouse between the domain and the store.)

repository.js

Adapter: wrap something with the wrong interface so it fits the one you need. Every driven adapter in the hexagon was one.

adapter.js

Observer: a subject keeps a list of observers and notifies them when it changes. It is the event bus in miniature, and also how DOM events (addEventListener) and most UI frameworks work.

observer.js

The second observer unsubscribed, so the last take only updates the shop page. Forgetting to unsubscribe is a classic memory leak.

Strategy: a family of interchangeable algorithms behind one interface, chosen at run time. In JavaScript, usually just functions in an object.

strategy.js

Add an overnight strategy. Nothing else has to change.

Patterns are a shared vocabulary, not a checklist. "Put a repository in front of it" says a paragraph in four words. Using one where a plain function would do is just extra code.

12 · DECISION RECORDS

Writing it down: decision records and C4

Architecture lives in people's heads until they leave. Two lightweight habits fix that.

An architecture decision record (ADR), as Michael Nygard proposed in 2011, is a short text file kept in the repository: one decision per file, numbered, never edited once accepted. When a decision changes, you write a new ADR that supersedes the old one, so the history of why survives. The tablets over the city are three of them; the grey one was superseded.

docs/adr/0003-payments-via-events.md
# ADR 3: Payments learns about orders only through events
 
Date:    2026-03-02
Status:  Accepted. Supersedes ADR 2.
 
## Context
Payments reads the orders tables directly (ADR 2 allowed one
shared schema). Every orders migration now breaks payments, and
the two teams must release together.
 
## Decision
Orders publishes OrderPlaced and OrderCancelled events.
Payments keeps its own table of what it needs and never reads
orders' tables. Enforced by an architecture test in CI.
 
## Consequences
+ Orders can change its tables freely.
+ Payments keeps working while orders is down.
- Payments' copy can be a few seconds behind.
- One more moving part: the event bus must be monitored.

The Consequences section is the honest part: write down what gets worse, not just what gets better. A decision with no downsides listed hasn't been thought through.

A decision that can be checked by a machine should be. This is an architecture test (the book Building Evolutionary Architectures calls these fitness functions):

architecture-test.js

Add ["infra/postgres.js", "web/routes.js"] to the list: infrastructure reaching up into the web layer. Does the test catch it?

For pictures, use Simon Brown's C4 model: four zoom levels, like a map. Context (the system and the people and systems around it), Containers (the deployable things: web app, database, mobile app), Components (the modules inside one container) and Code. Each diagram has a title, a key, and labelled arrows, which is more than most whiteboard drawings manage. The tool Structurizr lets you write the model as text and generate the diagrams:

workspace.dsl · Structurizr DSL
workspace {
  model {
    shopper = person "Shopper"
    shop = softwareSystem "Bookshop" {
      web = container "Web app" "Pages and API" "Node.js"
      db  = container "Database" "Orders, books, accounts" "PostgreSQL"
    }
    pay = softwareSystem "Card processor" "External"
    shopper -> web "Browses and buys"
    web -> db "Reads and writes"
    web -> pay "Charges cards"
  }
  views {
    systemContext shop { include * }
    container shop { include * }
  }
}

13 · CONWAY'S LAW

Conway's law: teams shape systems

In 1968 Melvin Conway wrote that organisations which design systems are constrained to produce designs which are copies of their own communication structures. His own example: eight people were to write a COBOL and an ALGOL compiler; five were put on COBOL and three on ALGOL. The COBOL compiler ran in five phases, the ALGOL compiler in three.

In the 3D, three teams float over the city. Each road between two districts owned by the same team glows green: they can change it at their own desk. Each road that crosses a team line glows red: changing it needs a meeting, a shared plan and a coordinated release. Which split of modules gives the fewest red roads?

conway.js

The search finds the split the 3D shows: catalog and cart together, orders and payments, shipping and accounts. Orders depends on four modules, so some red roads are unavoidable.

The practical lesson is the inverse Conway maneuver: if you want a certain architecture, organise the teams that way first, and the code will follow. The book Team Topologies builds a whole approach on it. It also works in reverse as a warning: microservices owned by one small team give you all the network costs and none of the independence.

14 · GROWING IT

Growing a codebase without rewriting it

Sooner or later someone proposes the big rewrite: stop, build it again properly, switch over. It almost always takes far longer than planned, while the old system still needs fixes and the new one chases a moving target. The safer path is to change the city while people live in it.

Fowler named it the strangler fig, after a vine that grows around a host tree until it can stand on its own. In the 3D: a router sits in front of the legacy block. One district at a time is rebuilt as a new service and the router sends its traffic there; when the last one moves, the old block can be switched off. At every moment, the shop is running.

strangler.js

Change trial to 100 and move "/cart" into migrated: that is a route finishing its migration. Hashing the user name means each person sees the same version on every visit.

The same idea works inside one program. Branch by abstraction: put an interface in front of the code you want to replace, make every caller use it, build the new implementation behind it, switch over (behind a feature flag), delete the old one. Each step is a small commit that keeps the tests green, so it can go straight to the main branch; see the Git page for why that matters. Your deploy pipeline is what makes many small, safe steps cheap.

And the everyday habit underneath all of it: leave each module a little cleaner than you found it, write the ADR when you make a decision, keep the architecture tests running, and split a module only when you can name what the split buys you. Architecture is not a phase at the start of a project. It is the shape you keep the code in, one change at a time.

PRACTICE

Now you do it

Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.

1

Hide the state

Anyone can write c.count = 99 on this counter and break it. Rewrite createCounter so the count is private (the object has no count property), and add a reset() that sets it back to 0.

exercise-1.js
2

Find the dependency cycle

Module cycles (orders needs billing, billing needs accounts, accounts needs orders) make code impossible to understand, test or split one piece at a time. Write hasCycle(graph): true if following the arrows can bring you back to where you started.

exercise-2.js
3

Write an adapter

The core needs an orders port with save(order) and find(id). All you have is an old key-value store with put(key, text) and get(key), which only holds text. Write ordersFromKv(kv): store each order as JSON under the key "order:" + id. Don't change repeatOrder.

exercise-3.js
4

Invert the dependency

This report reaches out for a real clock and a real mailer, so it can't be tested. Change sendDailyReport to take them as a second argument, { mailer, clock }, so the test at the bottom passes.

exercise-4.js
5

Project a read model

The write side recorded these events. Write the projection stockLevels(events) that returns how many copies of each book are on the shelf.

exercise-5.js
6

Replace the if-chain with strategies

Every new discount means editing priceFor. Move the rules into an object called discounts (one function per kind), add a student rule (15% off), and keep priceFor working, including for kinds with no discount.

exercise-6.js

RECALL

Check yourself, from memory

Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.

In one sentence, what makes a decision "architectural"?

Coupling and cohesion: which do you want high, and which low?

In layers, or in ports and adapters, which way do source-code dependencies point?

State the dependency inversion principle.

What does a modular monolith keep from microservices, and what does it avoid?

What is Conway's law, and what is the inverse Conway maneuver?

How does a strangler fig migration replace an old system?

GO FURTHER

Resources

ON THIS SITE

See it running

SOURCES

Where the facts come from

  1. Martin Fowler, Who Needs an Architect? (IEEE Software, 2003) "Things that people perceive as hard to change"; Ralph Johnson's "the important stuff"; and removing architecture by eliminating irreversibility.
  2. Martin Fowler, Software Architecture Guide
  3. Bass, Clements, Kazman: Software Architecture in Practice, 4th ed. (2021) Architecture as the structures needed to reason about a system: elements, relations and their properties.
  4. D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules (CACM, 1972) Information hiding: split a system by the decisions likely to change.
  5. Stevens, Myers, Constantine: Structured Design (IBM Systems Journal, 1974) Where coupling and cohesion were named.
  6. John Ousterhout, A Philosophy of Software Design Deep modules: a simple interface over a lot of functionality.
  7. Eric Evans, Domain-Driven Design (2003)
  8. Martin Fowler: Bounded Context
  9. Software package metrics (instability, I = Ce / (Ca + Ce)) Robert C. Martin's coupling metrics.
  10. Alistair Cockburn: Hexagonal Architecture (2005)
  11. Robert C. Martin: The Clean Architecture (2012) The dependency rule: source code dependencies point only inwards.
  12. Dependency inversion principle
  13. Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
  14. James Lewis, Martin Fowler: Microservices (2014)
  15. Martin Fowler: MonolithFirst
  16. Martin Fowler: Microservice Premium
  17. Shopify Engineering: Deconstructing the Monolith (2019) Why Shopify chose a modular monolith over microservices.
  18. Segment: Goodbye Microservices (2018) Over 140 services consolidated back into one.
  19. Latency numbers every programmer should know Main memory reference 100 ns; round trip within a datacenter 500 µs.
  20. Fallacies of distributed computing
  21. Martin Fowler: What do you mean by "Event-Driven"? (2017)
  22. Martin Fowler: CQRS "For most systems CQRS adds risky complexity."
  23. Martin Fowler: Event Sourcing
  24. Martin Fowler: Repository (Patterns of Enterprise Application Architecture)
  25. Gamma, Helm, Johnson, Vlissides: Design Patterns (1994) The source of Adapter, Observer and Strategy.
  26. Ford, Parsons, Kua: Building Evolutionary Architectures Fitness functions: automated checks that keep an architecture's properties true.
  27. Michael Nygard: Documenting Architecture Decisions (2011)
  28. Structurizr DSL (C4 diagrams as code)
  29. Melvin Conway: How Do Committees Invent? (Datamation, 1968)
  30. Martin Fowler: Conway's Law Including the inverse Conway maneuver.
  31. Skelton, Pais: Team Topologies
  32. Martin Fowler: Strangler Fig Application
  33. Martin Fowler: Branch By Abstraction