Before you startSoftware Architecture: The Town Plan#
A very short book for lead engineers who are asked to choose an architecture, say what the choice costs, write down why, and keep it true after they have left the room.
The promise
Read this in two evenings and you will be able to name the three qualities that decide a system's shape, analyse a trade-off in the open instead of by taste, recognise each of the common system-level styles and say what it is good and bad at, choose a style per system instead of per company, record the decision so that it outlives the people who made it, draw it so that others can read it, and keep it true with checks that run on every change.
It reads as one story. Bramble, an online shop, buys Quill, a wholesaler whose software is fifteen years old, works, and has almost none of its decisions written down. The failures in this book begin as decisions, even when they end as outages: one nobody can explain, one that cannot be made because nobody knows what matters, one made by fashion, one forgotten and argued three times, one silently reversed by a pull request that every reviewer approved. Each chapter opens with such a decision, the fix is the chapter's idea, and by the end of the chapter a new difficulty has appeared.
Every idea arrives four ways: a place in one running metaphor (a town and its buildings), a diagram of how it actually works, a one-sentence Remember it as line you can repeat out loud, and a dated account of who thought of it, when, and what they were reacting to. Every chapter then does what a mentor would do at a whiteboard: it names the traps and their tell-tale signs, gives you a small artefact with one planted flaw to find, and hands you three questions to ask whoever proposes the idea next, plus the one point on which experienced people still disagree, argued fairly from both sides.
Who this is for
You lead a team, or are about to. You have been asked "monolith or microservices?" as if those were the only two answers, and you have sat in a meeting where the same design decision was argued for the third time because nobody could find why it was made. You want the part of the field that lets you choose well, explain the cost and make the choice stick, without reading two thousand pages.
If you already run architecture reviews, write decision records as a habit and have fitness functions in your build, this book will be too slow for you; skim the At the whiteboard boxes, chapter 13's comparison table and the epilogue.
How to read it
- Straight through the first time. Characteristics and trade-offs come before any style, because a style is only good relative to them; the styles run from one deployable to many, which is also the order of rising cost; choosing comes after you have met every candidate.
- The boxes are where the book becomes a tool. The traps is a checklist. Spot it trains your eye; cover the answer. At the whiteboard is what to say in the meeting.
- The style chapters (5 to 11) each answer the same five questions and draw the same five capabilities, so any two can be laid side by side.
- The Epilogue is the reference: the metaphor table, the full review checklist, the style comparison table, two blank templates, the diagram legend, the timeline and a reading list by year.
The cast
Bramble sells stationery online and hosts marketplace sellers; it is built as a handful of services and one modular monolith, which is one deployable program whose internal borders are enforced. Quill sells office supplies to four thousand business accounts, each with its own contract prices, from one fifteen-year-old program and a batch that runs every night. Ana is head of architecture for both, and can no longer be in every room. Kofi is the staff engineer who measures things and remembers how they got that way. Ingrid has been Quill's architect for eleven years; she defends what works, and she knows why most of it is the way it is, though nobody ever asked her to write it down. Marcus is the consultant engaged to produce one target architecture for the merged company; his arguments for a single standard are the real ones, and when he is wrong it is because a premise fails here, not because he is foolish. Lena founded Bramble, bought Quill, and wants one company within a year.
Code, where it appears, is Java 21 and illustrates shape; it is not a buildable project. Artefacts (a scenario, a decision record) are shown in full, because the practice half of the book is about documents.
The sibling books
This is the third of three books about the same company, and each can be read alone. DDD and Microservices: The Atlas owns boundaries and integration: where to draw the borders inside a system, what the inside of one deployable looks like, and how services talk safely. System Design: The Depot owns data, scale and reliability: how to measure load, replicate, cache and stay up. This book owns styles, trade-offs and decisions. Where a chapter needs something a sibling owns, it gives one sentence and names the book and chapter.
Dates and sources
Every "who and when" in this book was checked against the primary source named in research/milestones.md in the book's repository, and the epilogue's reading list gives them by year. Where an idea has a famous date and a publication date that differ, the text uses the first public one and says so.
Contents
Act I: What architecture is for
- The Decision Nobody Remembered Making (what architecture is; significant decisions; accidental architecture)
- What Must This Building Survive? (quality attributes; driving characteristics; scenarios)
- Nothing Is Free (trade-off analysis; one-way and two-way doors; the last responsible moment)
- What Belongs Together (cohesion, coupling, connascence; components; the architecture quantum)
Act II: One building
- The Terrace (the layered style; open and closed layers)
- The Assembly Line (the pipeline style)
- The Hall and Its Stalls (the microkernel style)
Act III: Many buildings
- A Handful of Big Buildings (the service-based style; service-oriented architecture as history)
- The Noticeboard (the event-driven style; broker and mediator)
- Stalls That Carry Their Own Stock (the space-based style)
- Renting by the Hour (serverless)
- How Small Is Small Enough? (granularity; what pulls a service apart and what holds it together; microservices in a paragraph)
Act IV: Choosing and keeping
- Not One Plan for the Whole Town (choosing a style per system; the comparison table; hybrids)
- Write It Down (architecture decision records; design documents)
- Draw It So They Can Read It (the C4 model; notation)
- Keep It True (fitness functions; drift; technical debt; evolutionary architecture)
- The Architect in the Room (the role; how much control; the advice process)
- One Town, Three Plans (the architecture review)
Epilogue
- The Town Plan: the metaphor table, the review checklist, the style comparison table, the templates, the diagram legend, the timeline, the reading list
Chapter 1 · Act IThe Decision Nobody Remembered Making#
The problem
Six weeks after Bramble bought Quill, Lena asked for the first thing that would make them look like one company. A Bramble customer who also holds a Quill trade account should see their Quill contract price on Bramble's site. Kofi's team estimated two weeks: Quill's code has a pricing function, so call it from the product page.
It worked in test. In its first week live, a stationer in Leeds was quoted £41.20 for a case of copier paper on Bramble's site and invoiced £43.05 by Quill the next morning. Eleven more followed. The feature was switched off, twelve customers were refunded the difference, and two weeks became "we don't know".
Kofi found the cause in an afternoon. Quill does not work out a price when someone asks. Every night a batch recalculates every account's price for every product that account may buy, and writes the answers into one table of about nine million rows. Order entry reads the table. The spreadsheet upload reads the table. The invoices read the table. The function Kofi's team had called was a step from the middle of that batch, and at three in the afternoon it was reading supplier costs that had arrived since midnight and would not count until the next run.
Nothing on Quill's wiki said so. Ingrid knew. In her first year, eleven years ago, pricing an order as the customer typed it took eight seconds on the server they had, the suppliers sent their lists once a day, and the contracts promised that the morning's price would hold all day. So prices were fixed once a night. "Nobody asked," she said.
The idea
Architecture is the set of significant decisions about a system: the ones that are hard to change and expensive to get wrong. In the town this book is built in, architecture is the load-bearing walls, what is expensive to move once people live there. Design is the furniture, moved on a Saturday. "Prices are fixed once a night and read from a table" is a wall. Whether the table's key is a number or a string is furniture.
The two are ends of a spectrum, and there is one test for where a decision sits: what would it cost to reverse a year from now, and how many teams would have to agree?
| Architecture | Design | |
|---|---|---|
| The question | What is expensive to move? | What is the best arrangement today? |
| At Quill | Prices are fixed nightly; one database; one release for everything | The name of the price table; which class rounds to the penny |
| To reverse it | Months, several teams, customers notice | A pull request |
| Who must agree | Everyone who built on it | The team that owns the code |
| If the reason is lost | The next change breaks something far away | Nothing much |
Most people asked for a system's architecture describe its structure, the boxes and how they connect. That is one of four dimensions, as Richards and Ford list them. Structure is the style: Quill is a layered program and a nightly batch. Characteristics are what the system must be good at, which is chapter 2. Decisions are the rules that hold everywhere: prices are fixed nightly. Principles are guidelines rather than rules: prefer a batch step to a call between programs. The wall Kofi's team walked into was not in the structure. A diagram of Quill's boxes would not have shown it. It was a decision.
The diagram shows why the decision is architectural. Four parts of Quill were built on it over eleven years, each for a sensible local reason. The upload format has no price column because the table supplies one. Invoicing does not recheck a price because the table cannot have changed since the order. Move the wall and each of those is a change, a test and a conversation with customers.
Quill's wall was not a mistake. It was reasonable on the day. What went wrong is that it was never recognised as a wall, so it was never recorded, and that is accidental architecture: a house extended by every owner, with no drawings. Accidental does not mean bad. It means nobody can tell a wall from a partition without knocking on it, and Kofi's team knocked with a release.
An unrecorded decision has three costs, and Quill paid all of them in one week. Nobody could tell what was resting on it. Nobody could tell whether its reasons still held: on today's hardware an order prices in a few milliseconds, so the first reason has expired, while the supplier feed and the contracts have not changed. And the only copy of the reasons was one person, who was not asked because nobody knew there was a question.
The rule, then: write down the reason for anything that is hard to change. It has a limit. Recording every choice buries the walls among the furniture, and a team that writes a page for a variable name will stop reading its own pages. Record what fails the reversal test, and let the rest live in the code.
Remember it as: architecture is the walls you cannot move on a Saturday, and the reasons they were put there.
What Ana learned
- Ask what a decision would cost to reverse before calling it design, and treat it as a wall when the answer is more than one team and one month.
- Write the reason beside every wall, because the reason is what tells the next person whether the wall can come down.
- Ask the person who has been there longest what the system assumes before promising a date for anything that touches it.
…but
Lena's reply was short: so move the wall and make pricing live. Ingrid asked three questions back. Live for whom? How quickly? And is the invoice still allowed to be right? Nobody in the room could say what Quill's system has to be good at. They only knew what it does.
Chapter 2 · Act IWhat Must This Building Survive?#
The problem
"Make pricing live" could not be decided, because nobody could say what would count as better. Lena brought in Marcus, the consultant who had advised Bramble before, to produce a target architecture for the merged company, and he began where a careful consultant begins: a workshop, with the standard quality model pinned up as a checklist, so that nothing would be overlooked.
By the end of the afternoon there were fourteen cards. Scalability, availability, security, performance, maintainability, portability and eight more, and every one had been voted high. Two teams took the cards away as their brief. Kofi's team spent a sprint putting a cache in front of the price table so that lookups would scale, for a system that takes three hundred orders an hour on its busiest day. The same team looks after the nightly batch, and that fortnight nobody was watching it grow: on a Monday it finished at 06:40. The warehouse opens at six. Forty people stood in it without pick lists, and the first lorries left late.
"The batch finishes before the warehouse opens" was on no card. Everyone at Quill knew it, so nobody had said it. Ingrid's own list was two items long: the invoice has to be right, and the run has to be done by six.
The idea
A system has things it does and things it must be good at. The second kind is called a quality attribute, or in the newer books an architecture characteristic; they are the same thing. In the town, a characteristic is what the building must survive: fire, crowds, floods, burglars. A hospital and a warehouse may have the same number of rooms. They are built differently because of what each must survive.
The two are often written in one list and confused:
| Requirement | Characteristic | |
|---|---|---|
| Says | What the system does | How well, and under what conditions |
| At Quill | An account gets its contract price | A changed price is in force within five minutes |
| Lives in | One feature | The structure, across every feature |
| When it is wrong | Fix the feature | Change the architecture |
| Usually stated | In writing, by whoever wants it | Not at all, until it is missed |
Some characteristics are explicit: Lena's "one company within a year" is a demand for systems that can be changed quickly. Many are implicit. Nobody writes "the invoice is right to the penny" because nobody imagines otherwise. The useful question for finding implicit ones is what would get someone phoned at home.
They come in three kinds. Operational characteristics are about how the system runs: availability, performance, scalability, recovery after a failure. Structural ones are about how it is built and changed: modularity, testability, how easily it deploys. Cross-cutting ones apply everywhere and belong to no part: security, auditability, what the law requires.
Marcus's cards failed because they ranked nothing. Each characteristic you design for costs structure, and they conflict. Every security check is a delay. Elasticity argues for many small parts and simplicity argues for one. At Quill, a price that changes within minutes pulls against an invoice that anyone can reproduce a year later. So an architecture is shaped by its driving characteristics: the three that decide the building, because nobody can afford all of them. Three is a rule of thumb with a limit. A regulator can hand you a fourth that you may not trade away, and then you have four, and you pay for it in a narrower choice of structures. Past that, every addition removes candidates until the only design left is one that does everything moderately.
Ana ran the session again with one question: if we could only be good at three things, which? Quill's answers were that an invoice can be reproduced to the penny, that the nightly run is finished by six even on a bad night, and, new since the acquisition, that a price can change during the day. Just as usefully, the session said what Quill is not designed for: sudden crowds and round-the-clock availability. Kofi's cache came out.
A named characteristic is still only a word. "Changeable pricing" can mean anything. A quality-attribute scenario makes it testable, in the way a fire officer does: "when the alarm sounds with 300 people inside, everyone is out in four minutes." It states a stimulus, the environment it arrives in, the response and a measure.
Written out as the document Ana's team keeps:
Scenario: a contract price changes during the day
Source: an account manager at Quill
Stimulus: changes one account's discount band
Environment: a normal trading day, about 300 orders an hour
Response: the next quote and the next order for that account use the new price, and the invoice shows the price that was quoted
Measure: in force within 5 minutes for 99 changes in 100; no invoice differs from its quote by a penny
The measure is what turns an opinion into a test. The environment matters as much: "within five minutes" during the nightly run is a different and much more expensive promise, and this scenario does not make it. How to take such measurements in a running system belongs to the system design book, The Depot, chapters 1 and 18.
Remember it as: name the three things the building must survive, and for each say what happens, under what conditions, and what number proves it.
What Ana learned
- Separate what the system does from how well it must do it, and take the second list to the people who run it at night.
- Pick three driving characteristics for each system and say aloud which qualities you are not designing for.
- Turn each driver into a scenario with a number before anyone draws a box.
…but
With three drivers named, Marcus had a proposal within a week: retire the nightly price table and put a pricing service in its place that works out each price when asked and announces every change as an event. It passes the price-change scenario with four minutes to spare. Everyone in the room liked it, and nobody asked what it gives up.
Chapter 3 · Act INothing Is Free#
The problem
Marcus's proposal fitted on one slide. Retire the nightly price table. Put a pricing service in its place that works out a price when asked and publishes every change as an event. The slide had a column headed Benefits: live prices, the new scenario passed, no nine-million-row recalculation, a design that will scale. He wanted a decision that month, and he had a reason: every week without one, two teams built on the old assumption.
Ingrid was against it and could not say why in a form anyone could act on. "It will break invoicing" is a feeling until someone shows how. Her advice was to wait until more was known.
Ana had seen a benefits column before: Bramble had once split two services on the strength of one, and later merged them back. So she did not vote. She put the three scenarios from the previous chapter on the wall and asked the same question of each: what does this proposal do to it?
The price-change scenario passed. The six o'clock scenario improved. At the third, an invoice anyone can reproduce to the penny, the room went quiet, and then Ingrid could say it. Invoices are run from the same table the order was taken against, and every night's table is kept for seven years. When an auditor asks why account 2217 was charged £43.05 on the third of March, somebody looks it up. Under the proposal there is nothing to look up. The price exists for the length of one request. Marcus's team had a prototype pricing two test accounts that way, and when Ana asked finance to reproduce one of its invoices from the week before, they could not.
The idea
The honest answer to most architecture questions is "it depends", and it is useless unless you say on what. A trade-off is the cathedral ceiling and the heating bill: you may have the ceiling, and you will pay the bill, and the only real mistake is not knowing that the two arrive together. Trade-off analysis is the habit of taking each option to each driving scenario and writing down what it does there, losses beside gains. Marcus's slide was not wrong. It was half a table.
The formal version is the architecture trade-off analysis method, from the Software Engineering Institute. Run in full it takes days and a room of stakeholders, which is more than most decisions deserve. Its vocabulary is worth having for any decision:
- A utility tree breaks "a good system" into characteristics and then into scenarios, each rated for how much it matters and how hard it is to achieve.
- A sensitivity point is a decision that one characteristic depends on heavily.
- A trade-off point is a decision that is a sensitivity point for two characteristics, in opposite directions. These are the decisions worth a meeting.
Quill's tree has one trade-off point, and it is the wall from chapter 1. Fix the price at midnight and every invoice can be reproduced, but nothing changes during the day. Fix it at the moment of asking and it can change at will, but nothing records what it was.
A cheaper way to find such points is risk storming. Put the diagram of the proposal on a wall. Everyone, alone and without talking, sticks a note on each place they think it will go wrong, coloured by how likely and how bad. Then compare. Where the notes pile up there is a risk, and where one person has put a red note that nobody else did there is usually a piece of knowledge the room lacks. At Quill, Ingrid's lone red note was on the arrow from invoicing to the price table.
Having found the cost, Ana still had to decide, and here two more ideas do the work. A two-way door is hanging a door: if it is in the wrong place, you move it. A one-way door is pouring the foundations. Marcus's proposal looked like one decision and was two. Working out a price on request, for quotes, is a two-way door: the table stays, and if the new path misbehaves it is switched off. Retiring the table is a one-way door: seven years of audit answers and an upload format with no price column rest on it. The rule is to decide two-way doors quickly and close to the work, and one-way doors slowly and in the open. Its limit is the case where delay is itself the one-way door, a contract that lapses or a competitor that ships, and then you walk through knowingly and write down what you could not check.
For the one-way door, the question is when. The last responsible moment is ordering the windows when the walls are up: not before, when you would be guessing the sizes, and not after the rain has come in. It is the point after which not deciding removes an option. For the price table, that moment is when invoicing itself is next rebuilt, and not this month. Deferring has a cost too, because people are waiting, so the moment gets a name and a date instead of "later".
The decision Ana took was neither proposal nor refusal. Quotes are worked out on request, the quoted price is stored on the order line, and the nightly table stays as the record invoices are checked against. Marcus's urgency would have been right had the whole proposal been a two-way door, and Ingrid's wish to wait would have been right had nothing been blocked in the meantime. That is not the best architecture. There is no best one. It is the least-worst: the option whose losses, for these three drivers, the company can most easily live with. Its loss is written down too: a price is now worked out in two places, and they must agree.
Remember it as: ask of every proposal what it gives up; if the answer is nothing, you have not found it yet.
What Ana learned
- Take every proposal to each driving scenario and write what it does there, with the losses beside the gains.
- Split a proposal into its doors, decide the two-way ones this week, and give each one-way door a date.
- Choose the least-worst option for the three drivers, and write down the loss you accepted.
…but
The two-way door needs the nightly run and the new request path to produce the same price to the penny, every time. "Then you had better find out where pricing lives," Ingrid said. "It isn't in one place." Last month's change to a single rounding rule had touched thirty-one files.
Chapter 4 · Act IWhat Belongs Together#
The problem
Kofi's team went looking for where pricing lives. It lives in quill-common, a library that also holds date formatting and the code that sends email, and that library is compiled into three programs: order entry, the nightly batch and the reporting tool. The class that prices a line is the same class in all three, by copy.
Finance had asked for one change to it: round each case to the penny, not each order line. The change was eight lines. It was also thirty-one files, because the price passes through every layer of every program on its way to a screen or a table. And it had to go out in all three programs on the same night.
It did not. Order entry was released on the Thursday. The batch was held back by a failing test and went out the following Tuesday. For five nights quotes were rounded one way and the price table the other, and 212 invoices went out a penny or two away from what the customer had been shown. Nothing had crashed. Each program was correct by its own tests.
Marcus's answer was the standard one: a rule should exist in one place, so make pricing a single shared thing that everybody uses. Ingrid pointed at the library. "That is what this was."
The idea
Cohesion is about the inside of one unit: rooms that belong in one building. Pricing rules, discount bands and rounding belong together. Pricing, date formatting and email do not, and a library that holds all three will be changed for three unrelated reasons.
Coupling is about the space between units: buildings that share a wall, a boiler or a drain. It has a direction. Afferent coupling counts who depends on you, and tells you how dangerous you are to change. Efferent coupling counts whom you depend on, and tells you how easily you break. quill-common has an afferent coupling of three programs, so every change to it is three releases.
Connascence says what the other two leave vague. Two things are connascent when a change to one requires a change to the other for the system to stay correct: the lock and every copy of the key. It comes in kinds. Static kinds can be seen by reading the code: two places must agree on a name, a type, the meaning of a value (prices are in pence), the order of arguments, or an algorithm. Dynamic kinds exist only while the system runs: two things must happen in a certain order, within a certain time, with values that match, or on the very same object. And it has three properties:
- Strength: how hard the dependence is to see and to change. A shared name is weak; a shared algorithm is stronger; anything dynamic is stronger still.
- Locality: how far apart the two things are. Inside one class, even strong connascence is harmless. Across teams or across a network it is not.
- Degree: how many things are bound. Two is manageable. Thirty-one files in three programs is not.
Quill's five bad nights were connascence of algorithm, at a distance, with a degree of three: three programs that had to round identically and could be released on different days.
| Cohesion | Coupling | Connascence | |
|---|---|---|---|
| Asks | Do the things inside belong together? | How much does this unit depend on others? | Exactly what must change together? |
| Looks | Within one unit | Between units: afferent in, efferent out | Between any two elements |
| At Quill | The library holds pricing, dates and email: low | Three programs depend on it | All three must round the same way |
| You want | High | Low, or at least known | Weak, or else close together |
The rule that follows is to keep strong connascence close and let only the weak kind travel. Its limit is that weakening always costs something, usually a layer of indirection, and inside one small unit that cost buys nothing.
Two more words turn this into something an architect can arrange. A component is a wing with its own entrance: a named part of the system with one job and a stated way in, whether it is built as a package, a module or a service. How the components are cut is the first structural decision. Technical partitioning cuts by kind of code: presentation, business logic, persistence. Domain partitioning cuts by what the business does: pricing, ordering, invoicing. Quill is partitioned technically, which is why one pricing rule crossed thirty-one files. Technical partitioning is the right cut when the changes you expect are technical ones, such as replacing the database. Most changes are not. How to find the domain's own borders is the Atlas's subject, chapter 6.
The last word is the one the rest of this book leans on. An architecture quantum is what must be built, opened and closed together: the smallest part of a system that can be deployed on its own, counting everything it cannot run without, its database included. Quill has three programs and one quantum, because all three share a database and a library. That is why the release in two halves failed, and why characteristics are chosen per quantum. Inside one quantum, everything gets the same availability, the same release day and the same bad night.
So the repair was modest. Pricing became one component with one entrance and one owning team. The reporting tool stopped pricing anything and now reads stored prices, which takes the degree from three to two. And the release process refuses to ship order entry without the batch, which admits in the open what was always true: they are one quantum. Marcus was right that the rule needed one home. Ingrid was right that a library copied into three programs is not one, and his argument fails only where the single thing is shared by copying.
Remember it as: keep together what changes together, and make sure that what is far apart can change alone.
What Ana learned
- When something breaks far from the change, name the connascence: what had to change together, how strongly, how far apart and how many.
- Cut components by what the business changes, and keep the technical layers inside them.
- Draw the quantum before promising that anything is independent, because whatever shares a database or a library ships together.
…but
Marcus's interim report arrived the same week. It called Quill "a legacy layered monolith" and named the layering as the root cause. Ingrid read it standing up. "It is one building with floors. It has taken three hundred orders an hour for fifteen years without anyone thinking about it. Which part of that is wrong?"
Chapter 5 · Act IIThe Terrace#
The problem
Marcus's report said that layers group code by what it is and not by what changes together, so that every business change is smeared across the whole program, and it recommended replacing the layered design. Ingrid's reply was that layers are simple, cheap, understood by every developer Quill has ever hired, and have carried order entry for fifteen years. Each was repeating a position with a long history, and Ana had no way to choose between them except by liking one of them more.
So Kofi counted. He took the last forty changes made to order entry. Thirty-three had touched one screen and the code directly beneath it, and had been done by one developer in under two days. Seven had been pricing changes, and those were the thirty-one-file kind. Then he traced two hundred kinds of request through the program. Forty-six of them passed through the middle layer without the middle layer doing anything to them at all.
He also found something nobody had decided. The reporting tool does not go through the business layer. In 2014 someone made one report faster by reading the tables directly, and the trick was copied into every report since. Last year a discount rule was added to the business layer, the reports never saw it, and an account manager walked into a contract renewal with a spend figure £1,900 lower than the customer's own invoices.
The idea
The layered style is the terrace: every house has the same floors in the same order. This chapter looks at it as the shape of a whole system. What the inside of one deployable should look like, and the styles that turn the layers inside out, belong to the Atlas, chapter 9.
The parts. Horizontal layers, each with one technical job: presentation, business, persistence, and the database underneath. Every feature the system has is cut across all of them. Quill's five capabilities do not appear as parts at all; each is a strip running down through every floor.
How they talk. A request enters at the top and is passed down by ordinary calls inside one process; the answer comes back up the same way. A layer is closed when a request must go through it and may not skip to the one below. Layers are closed by default, because closure is what the style sells: a change to how data is stored can reach the business layer and no further. An open layer is one that requests are allowed to bypass. Opening one is legitimate when the layer adds nothing for a whole class of requests, read-only reports being the usual case, and it has a price: everything above now depends on two layers instead of one, so a change below travels further. An open layer that was decided and written down is a design. One that was discovered, like Quill's reports, is a leak.
Where the data lives. In one database, reached only through the persistence layer. Nothing in the style says which tables belong to which capability, so in time every query joins whatever is convenient.
Strong and weak. The style is strong on simplicity and on cost: one program, one build, one thing to run, and no developer needs it explained. Testability is fair, since a layer can be tested against a stand-in for the one below. It is weak on deployability, because every change ships the whole building; on elasticity and fault tolerance, because the building scales and fails as one; and on evolvability whenever the change is to the business and not to the technology.
The cost of leaving. Low if the layers have stayed closed: the same code can be re-cut by domain, one capability at a time. High if they have leaked, because the first job is discovering what depends on what.
When a request passes through a layer that does nothing but hand it on, that is a sinkhole: a floor you only walk through to reach the stairs. Some are normal. The working rule is about proportion: if roughly a fifth of requests are sinkholes, the layers are earning their keep on the rest; if most are, the system is paying for isolation it does not use. Order entry's forty-six in two hundred is acceptable. The reporting tool's figure would be nearly all of them, which is why somebody opened that layer by accident. The repair is to open it on purpose, for reports only, and to have reports read stored prices and never recompute one.
The alternative Marcus was reaching for has a name, the modular monolith: one building with fire walls between the wings.
| Layered | Modular monolith | |
|---|---|---|
| Cut by | Technical role | Business capability; each module keeps its own layers inside |
| A pricing change touches | Every layer | One module |
| Borders enforced | Between layers, by convention | Between modules, by the build |
| Data | One schema; any query may join any table | One database, a schema per module |
| Deploy unit | One | One |
The Atlas, chapters 9 and 21, covers how to build one and when it is the default.
So when is layered the right answer? When the team is small, the work is mostly data in and data out, changes stay within one screen and the code beneath it, and the driving characteristics are simplicity and cost. Thirty-three of order entry's last forty changes were exactly that. Order entry stays a terrace, and Ingrid was right about it. Marcus was right about the other seven: pricing changes cut across the floors, which is why pricing became a component with its own entrance in the last chapter. His premise, that business changes cross every layer, fails here only because most of order entry's changes are not that kind. In a system where most are, with several teams, it holds.
Remember it as: every house has the same floors in the same order: cheap to build, and a change to the business climbs every floor.
What Ana learned
- Count before condemning a style: how many changes crossed its boundaries, and how many requests passed through a part that did nothing.
- Keep layers closed by default, and when one is opened, write down which, for what, and what it costs in isolation.
- Leave a simple system in a simple style when its drivers are simplicity and cost.
…but
The report had called the nightly batch layered as well. It is not layers. It is six thousand lines that do one thing after another, and that night it stopped at 03:10 on a malformed row in one supplier's price list. There was no way to restart it from the middle. It began again from the top and finished at 07:25.
Chapter 6 · Act IIThe Assembly Line#
The problem
The nightly run is one procedure of six thousand lines. It starts at one in the morning, when the last supplier's file is due, and takes about four hours. It works through thirty-eight suppliers in turn, importing each list and recalculating the prices that depend on it, then prices the day's uploaded orders, produces the invoices and sends the couriers their manifests, sharing a set of working tables and several running totals held in memory.
At 03:10 it met a price written 4,85 by a new supplier in Rotterdam and stopped. Twenty-two suppliers had already been imported. Nobody could restart it from the twenty-third, because nobody could say which working tables were half written and which totals had been lost with the process. Running it again from that point risked applying twenty-two price lists twice. So it ran again from the top and finished at 07:25. The warehouse stood for eighty-five minutes. It was the third overrun of the quarter, and the second driving characteristic from chapter 2, finished by six, had now been failed in public.
Ingrid explained the design without defending it. When she joined, the whole run took forty minutes, and starting again cost nothing anyone noticed. Marcus proposed removing the batch altogether: treat every price change as an event in a stream, and there is no window to miss. Ingrid's answer was that the suppliers send one file a day whatever Quill builds, and that recomputing from those files each night is the one design she knows how to prove correct.
The idea
Work that flows in one direction, through steps that each do one thing, has its own style. The pipeline style, also called pipes and filters, is the assembly line: each station does one thing and passes it on.
The parts. Filters and pipes. A filter is a station; a pipe is the conveyor between two stations. Filters come in four kinds. A producer starts the line: it reads a supplier's file. A transformer takes what arrives, changes it and passes it on: convert the cost to pounds, apply the account's contract terms. A tester checks each item and either passes it on or diverts it: a row that cannot be parsed goes to a rejects pile. A consumer ends the line: it writes the price table or sends a manifest.
How they talk. One way only. A filter reads from the pipe behind it and writes to the pipe in front. It does not know which station comes next, never calls another filter, and never reaches back for something an earlier one had. The pipe carries one agreed format. In code the whole contract is a single method:
public record PriceListRow(String supplier, String sku, Money cost,
LocalDate validFrom) {}
public interface PriceListFilter {
Stream<PriceListRow> apply(Stream<PriceListRow> rows);
}
And one transformer, which keeps nothing between one row and the next:
public final class ToSterling implements PriceListFilter {
private final Map<Currency, BigDecimal> ratesAtMidnight;
public ToSterling(Map<Currency, BigDecimal> ratesAtMidnight) {
this.ratesAtMidnight = Map.copyOf(ratesAtMidnight);
}
@Override
public Stream<PriceListRow> apply(Stream<PriceListRow> rows) {
return rows.map(r -> new PriceListRow(
r.supplier(), r.sku(), toSterling(r.cost()), r.validFrom()));
}
private Money toSterling(Money cost) {
var rate = ratesAtMidnight.get(cost.currency());
long pence = rate.multiply(BigDecimal.valueOf(cost.minorUnits()))
.setScale(0, RoundingMode.HALF_EVEN).longValueExact();
return new Money(pence, Currency.getInstance("GBP"));
}
}
Money is the record the Atlas defines in its chapter 10, an amount in minor units and a currency. The line itself is a list of such filters applied in order.
Where the data lives. In the pipes. Between stations the work in progress sits in the pipe, and for a nightly run the pipe should be something that survives a crash: a file or a staging table named for the run and the station. The filters themselves own nothing.
Strong and weak. Strong on simplicity, testability and evolvability: a station is tested with a file in and a file out, and a new one is slotted in without touching the others. It is cheap to run. Fault tolerance is fair, and only if the re-run is designed. It is weak on deployability and elasticity when the line is one deployable, and it cannot hold a conversation. There is no way back up the line, so anything that must answer a waiting customer, such as taking an order at a screen, does not belong on it.
The cost of leaving. Low. Filters are small and know nothing of each other, so they can be moved into separate jobs or rented functions one at a time. The pipe format is the part worth keeping.
Re-running is where the style pays for itself, and it has to be built in. Each station writes its whole output to a durable pipe, or none of it. Each station can be run twice on the same input and leave the same result, replacing what it wrote before instead of adding to it. The run records which stations have finished, so a restart begins at the first that has not. And bad input is diverted by a tester, never thrown, so that one supplier's comma does not stop the other thirty-seven. The rule is that every station must be safe to repeat. Its limit is a consumer that acts on the outside world: a manifest sent to a courier twice is two collections. That station needs a record of what it has sent, or a reference the other side uses to discard repeats, and keeping that record is the cost.
A pipeline is not workflow orchestration. On a line, data moves one way through stations of one shape, and the only choice is pass or divert. In a workflow a coordinator runs steps of different kinds, waits for people or other systems, branches, and undoes earlier steps when a later one fails. The moment a design says "if station four fails, reverse station two", it has stopped being a pipeline and become the saga of the Atlas, chapter 16. A scheduler that starts the stations in order does not change the style.
Quill's run was rebuilt as fourteen stations. The batch stayed a batch, and Ingrid was right about that: the input arrives once a day, and the nightly price table is what makes an invoice reproducible. Marcus's stream is the better design where data arrives all day and answers are wanted all day, which the system design book takes up in chapter 12. The next malformed row went to the rejects pile, and that night's run finished at 04:50 as usual.
Remember it as: each station does one thing, keeps nothing and passes it on, so any station can be tested alone and run again.
What Ana learned
- Give work that flows one way an assembly line, and give every station one job and no memory.
- Design the re-run before the run: durable pipes, stations that can be repeated, and a record of which have finished.
- Divert bad input to a place someone reads, so that one row never stops the line.
…but
The last station is courier booking, and it is one class with a branch for each courier: Quill's three, and Bramble's two since the merger. Lena has announced a partner programme, twenty partners within the year. Adding the sixth courier last week changed a helper that formats labels, and for a morning the second courier's depot refused every parcel Quill sent.
Chapter 7 · Act IIThe Hall and Its Stalls#
The problem
Courier booking is one class with a branch for each courier. There are six branches now, and they share helpers: one for labels, one for postcodes, one for weights. The sixth courier, a pallet carrier taken on for Bramble's bulky orders, needed the label helper to print a longer reference. The change was tested against the new courier. The next morning the second courier's depot refused 340 parcels, because its scanners could not read the longer label.
Every courier added is a change to the code all the others run through, a retest of all of them, and a seat on the fortnightly release. Lena's partner programme promises twenty integrations within the year: more couriers, a tax calculator for sales to Ireland and the Netherlands, suppliers who want to send price lists in their own formats, and eventually partners who write their own.
Marcus proposed a service for each integration: separately deployed, with a published interface that cannot be bypassed, and a clear owner. Ingrid looked at what each one does. They all do the same job, in about two hundred lines, with a different courier at the far end. Twenty more things to deploy and watch, for twenty small variations of one thing, seemed to her the wrong size of answer.
The idea
When one job has many variations, and the variations come and go, the style that fits is the microkernel style, also called the plug-in architecture. It is the market hall and its stalls: the hall provides the roof and the rules, and the stalls come and go.
The parts. A core system and plug-ins. The core holds only what is always true: take a parcel that is ready, find a courier that can carry it, ask for a booking, record the result. A plug-in holds one variation: everything about one courier and nothing about any other. Two smaller parts make it work. The contract says what every plug-in must offer and what it is given. The registry is how the core finds out which plug-ins exist and what each can do.
How they talk. The core calls a plug-in through the contract, usually as an ordinary call inside one process. A plug-in never calls another plug-in, and it knows the core only by what the contract passes in.
public interface CourierPlugin {
String id();
int contractVersion();
boolean canCarry(Parcel parcel);
Booking book(Parcel parcel);
}
public record Parcel(OrderId orderId, int grams, String postcode,
Money declaredValue) {}
public record Booking(String courierId, String trackingNumber, Money charge) {}
The registry is a lookup, and it is also where a plug-in built for the wrong contract is turned away:
public final class PluginRegistry {
private static final int SUPPORTED = 2;
private final Map<String, CourierPlugin> plugins = new LinkedHashMap<>();
public void register(CourierPlugin plugin) {
if (plugin.contractVersion() != SUPPORTED)
throw new IllegalArgumentException(plugin.id()
+ " was built for contract " + plugin.contractVersion());
plugins.put(plugin.id(), plugin);
}
public Optional<CourierPlugin> firstAbleToCarry(Parcel parcel) {
return plugins.values().stream()
.filter(p -> p.canCarry(parcel)).findFirst();
}
}
One stall, complete:
public final class NorthlinePlugin implements CourierPlugin {
private final NorthlineClient client;
public NorthlinePlugin(NorthlineClient client) { this.client = client; }
@Override public String id() { return "northline"; }
@Override public int contractVersion() { return 2; }
@Override public boolean canCarry(Parcel parcel) {
return parcel.grams() <= 30_000;
}
@Override public Booking book(Parcel parcel) {
var label = client.createLabel(parcel.postcode(), parcel.grams());
return new Booking(id(), label.tracking(), label.charge());
}
}
Where the data lives. With the core. A plug-in is handed what it needs in the call and hands back a result. It has no connection to the core's tables. Anything of its own, such as a courier's service codes, it keeps itself.
Strong and weak. Strong on evolvability, testability and simplicity: a courier is added without touching the core or any other courier, and is tested alone against the contract. Cost is low. Deployability is fair: better than a plain monolith if stalls can be added without redeploying the hall, no better if they cannot. It is weak on elasticity and fault tolerance, because everything is under one roof.
The cost of leaving. Low. The contract is already a boundary, so a plug-in can be moved into its own process behind the same contract, one at a time. The exception is a core that has grown fat, which must be thinned first.
Two things about the contract decide whether the hall survives its tenth stall. The first is versioning. The contract will change; sales to Ireland need a customs declaration no courier call carries today. So the contract has a version number, each plug-in declares the version it was built for, and the registry refuses a mismatch at start-up instead of failing on a parcel at two in the morning. While one team writes every plug-in and they all ship in one release, the number is ceremony, and it is reasonable to leave it out; the cost is that it must be added on the day the first outsider writes a stall, and by then there are twenty to update.
The second is isolation. A stall that catches fire is inside the hall. Every call to a plug-in gets a time limit, and a failure is caught at the contract and recorded against that courier alone. If plug-ins are written by people you do not employ, they run in a separate process behind the same contract, which costs a network hop and something more to operate.
The style is easily confused with the one whose name it nearly shares:
| Microkernel | Microservices | |
|---|---|---|
| The parts | A core, and plug-ins that all do the same kind of job | Services that each do a different job |
| Who is in charge | The core calls the plug-ins | Nobody; services deal with each other as equals |
| Runs as | Usually one process | Many processes |
| Data | With the core | Each service owns its own |
| A part failing | Can take the hall down unless isolated | Is contained, if the others can cope without it |
| Suits | Many variations of one thing | Many different things with different owners |
Courier booking became a hall. The supplier formats did too, as the first station of chapter 6's line, which makes that station a small hall on an assembly line; styles combine, and chapter 13 returns to that. Ingrid's size of answer was the right one while one team writes every stall. Marcus's premise, that each integration needs its own deployment and owner, fails for six couriers maintained by four people. It will hold for the stalls that partners write themselves, and the contract is what will let those move out.
Remember it as: the hall provides the roof and the rules; a stall can arrive, change or leave without the hall or its neighbours noticing.
What Ana learned
- When one job has many variations, put the job in a small core and each variation in a plug-in behind one contract.
- Keep the core ignorant: no plug-in's name appears in it, and no plug-in sees its database.
- Version the contract before anyone outside the team writes a plug-in, and give every call to a plug-in a time limit.
…but
The hall took its seventh courier in a day. It is still inside one release. Three teams now work on Quill, order entry, pricing and finance, and every change any of them makes leaves on the same fortnightly train. A pricing fix has just waited nine days behind an invoicing change that failed its tests twice. One building has one front door.
Chapter 8 · Act IIIA Handful of Big Buildings#
The problem
Three teams work on Quill now: order entry, pricing and finance. All of their changes leave on one release every other Thursday, and a release goes only if every team's tests pass.
A fault in the volume-break calculation was over-discounting one large account, and the pricing team had the fix ready on a Monday. It sat for nine days behind an invoicing change that failed its tests twice. For nine days the account went on being over-discounted, and Quill gave away £6,400. Nobody had done anything wrong. The fix was simply in the same building as somebody else's unfinished work.
Marcus's proposal was to end the shared release for good: about thirty services, each with its own database and its own pipeline, none able to block another. Ingrid agreed about the release and not about the rest. The database has four hundred tables and fifteen years of queries that join across them. Month-end reconciliation is one query over orders, invoices, payments and credit notes. Giving each of thirty services its own data, she said, is two years' work, and she could not promise the month would still close at the end of it.
Ana asked which characteristic was actually failing. It was not scale and not resilience. It was that three teams could not ship separately.
The idea
There is a style for exactly that, and it is the least dramatic way to become a distributed system. The service-based style is a campus of a few large buildings sharing one records office.
The parts. A small number of coarse-grained domain services, usually between four and twelve. Each is a whole area of the business, is deployed on its own, and is an ordinary layered or modular program inside. A user interface sits in front of them, and one database sits behind.
How they talk. The user interface calls the services. The services call each other as little as possible, directly when they must, and there is nothing in the middle to route or translate. Much of what would be a call between services in other styles is here simply a read from the database.
Where the data lives. In one shared database, logically partitioned: the tables are grouped into a schema for each service, and each schema has exactly one service that writes to it. Other services may read it, by agreement. That rule is the whole difference between this style and the shared database the Atlas warns against in chapter 8, where nobody owns the tables. It has no exception worth having. Reading is another matter: reading another service's schema is allowed because it keeps reports and reconciliations simple, and every such read is a column its owner can no longer rename freely. So the reads are listed, and the list is kept short.
Strong and weak. Strong on deployability: each team ships on its own day. Simplicity and cost are fair, and the best of any distributed style, since there are few things to run and the data has not moved. Testability and evolvability are good, because each service is one area of the business. Fault tolerance and elasticity are only fair: a service can fail or be scaled alone, but every one of them stands on the same database.
The cost of leaving. Low in either direction. Each service with its schema is already cut along a business line, so it can be divided further, or take its tables away with it, when a reason arrives. The services can also be folded back into one program without touching the data.
The style is easiest to understand beside its two relatives. Service-oriented architecture is a campus where every errand goes through the central switchboard, and the switchboard was the enterprise service bus.
| Service-oriented architecture | Service-based | Microservices | |
|---|---|---|---|
| Service size | Mixed; many small services meant for reuse across the company | A whole business area; four to twelve of them | One capability; tens or hundreds |
| Data ownership | Shared company databases behind the services | One database, one schema and one writer per service | A database per service |
| Coordination | A central bus that routes, translates and runs the process | The screens, and a few direct calls | No centre: calls and events between equals |
| Deploy unit | The service, though most changes also touch the bus | The service | The service |
| Reacting to | Departmental systems that could not talk to each other | One release shared by several teams | The bus, and the shared database |
Service-oriented architecture is history now. Its aim was reuse: write "get customer" once and have every system call it through the bus. In practice the bus collected routing rules, then data conversions, then business rules, until every change in the company waited for the team that ran it. Microservices were the reaction: no centre, and nothing shared. The service-based style sits between the two on purpose. It takes separate deployment from microservices and declines to pay for separate data until a characteristic demands it.
Quill's back office became four services over five weeks. The work was not moving data. It was declaring an owner for each table and removing the twenty-three places where one team's code wrote to another's. Dispatch now books the courier as each order is placed, so that the customer can be told a delivery day, and the nightly line sends only the manifest. The pricing team now ships three times a week. Ingrid was right about the data. Marcus was right that the shared release had to end, and his premise, that separate deployment needs separate databases, fails only because deployment was the one thing failing. Where services must scale or fail separately, or one team's schema changes weekly, it holds.
Remember it as: a few big buildings, each with its own door, and its own shelves in one shared records office.
What Ana learned
- Name the characteristic that is failing before deciding how far to split; when it is three teams unable to ship separately, a few big services are enough.
- Give every table one writing service, and keep a short list of who else reads it.
- Split the code first, and the data only when a characteristic demands it.
…but
Each team now ships on its own day. But placing an order asks Pricing for a price, then Dispatch for a courier booking, one after the other, while the customer waits. Last Thursday one courier's system was down for forty minutes, and for forty minutes Quill could not take an order.
Chapter 9 · Act IIIThe Noticeboard#
The problem
Placing an order at Quill is now a chain. Ordering asks Pricing for a price, then asks Dispatch to book a courier, and Dispatch asks the courier's own system. The customer waits for all of it.
On Thursday one courier's system was down for forty minutes. Dispatch's calls to it timed out, Ordering's calls to Dispatch timed out behind them, and for forty minutes nobody could place an order. About 190 were lost or phoned in. The courier would not have collected anything until four that afternoon in any case.
There was a second, slower problem. Bramble's marketplace team wanted to know whenever a trade order was placed, to award loyalty points. Finance wanted to know, to track credit exposure. Each asked the Ordering team to add a call to their system. Ordering was acquiring a list of everyone who cared about orders, and every addition was another thing that could stop an order being taken.
Marcus's answer was his target architecture in one sentence: nothing calls anything; every service announces what it has done, and every other service listens. Ingrid's objection was two questions. When a customer is typing an order and needs a price, who are they supposed to wait for? And once nothing calls anything, who can say what happens after an order is placed?
The idea
The event-driven style is the town noticeboard: post a notice, and whoever cares acts on it. An event is a statement that something has happened, order placed, written by the one service that knows, without knowledge of who will read it.
The parts. Event processors, which are services that react to events and post their own; the events; and the channels that carry them, held by a message broker. The event that starts a flow is the initiating event. The ones posted along the way, courier booked, invoice opened, are processing events.
How they talk. Asynchronously. The poster does not wait. There are two ways to arrange who reacts, and they are different enough to count as two styles. In the broker topology, notices are passed hand to hand: each processor reacts to an event and posts another, and the flow is whatever chain of reactions results. In the mediator topology, a clerk reads each notice and assigns the errands: the initiating event goes to one component that knows the steps, tells each processor what to do, and keeps track.
| Broker | Mediator | |
|---|---|---|
| Who knows the whole flow | Nobody; it emerges | The mediator |
| Adding a reaction | Subscribe; nothing else changes | Change the mediator |
| When a step fails | Each processor copes alone | The mediator retries, reroutes or undoes |
| Processors depend on | The events | The mediator's instructions |
| Suits | Simple flows with many independent reactions | Flows with a fixed sequence, conditions and steps to undo |
The Atlas meets the same choice for sagas in chapter 16, under the names choreography and orchestration. Quill's order flow is a fan of independent reactions, so it uses a broker. Returns, which have a fixed sequence and steps to undo, use a mediator.
Where the data lives. With each processor. A service keeps what it needs, often as a copy built from the events it has read, so an event has to carry enough for its readers to act on. At Quill the services still share one database; the two styles combine.
Strong and weak. Strong on fault tolerance, because a processor that is slow or down delays only its own work while the notices wait for it; on elasticity, because each processor is scaled by the length of its queue; on performance as the customer feels it; and on evolvability, since a new reader needs no change to the poster. Weak on simplicity and testability: the flow is written down nowhere, and it behaves differently depending on timing. Weak, too, wherever the business needs one immediate answer that everyone agrees on.
The cost of leaving. High, in both directions. Every reader has been written to expect things late and sometimes twice, and how a poster makes sure a notice really is posted is a chapter of its own, the Atlas's fifteenth. Turning such a flow back into calls, or calls into such a flow, means redesigning it.
Error handling is the part that most often goes missing. In a call, the caller is standing there when it fails. In an asynchronous flow the poster left long ago, so the failure needs somewhere to go. Each channel gets a path: retry a set number of times, then park the event where a person or a program will look, and post an event that says so. Quill's Dispatch tries a courier three times, then posts booking failed, and Ordering reacts by offering the next courier that can carry the parcel.
Sometimes a service that only has the noticeboard needs an answer. Request-reply over events does it: post the question with a reference and a return address, and wait for a notice bearing the same reference. It works, and it is a call with extra steps. The rule is that when the caller cannot continue without the answer, it should call. The exception is a responder that is slow or often absent, where waiting on a channel beats holding a connection open; the cost is time limits and matching references that a plain call gives you free.
Because the style looks like the assembly line with more arrows, the two are worth separating:
| Pipeline | Event-driven | |
|---|---|---|
| Shape | One line in a fixed order | A web; any number of readers for each event |
| What moves | The work itself, changed at each station | Notices that something happened |
| The next step is known by | The line | Nobody, or the mediator |
| It ends | At the consumer, with a result | When nobody reacts any more |
Order taking at Quill no longer waits for a courier. Bramble's loyalty system subscribed to order placed and the Ordering team changed nothing. Marcus was right about everything that happens after the order. Ingrid was right about the price, which is still a call, and about visibility: every event now carries the order's reference, and the list of who reads each event is kept beside its definition.
Remember it as: post what happened and let whoever cares act on it; when you need an answer now, ask directly instead.
What Ana learned
- Call when the caller cannot continue without the answer, and post an event for everything that can happen afterwards.
- Design where a failed event goes before the successful path ships.
- Give each event one publisher, and keep the list of its readers where that publisher can see it.
…but
Lena has set the date for the first joint sale: Bramble's back-to-school week, with Quill's catalogue offered to two million Bramble customers. Kofi rehearsed it, with price and stock lookups at fifty times the normal rate for two hours. The database was on its knees within four minutes, and order entry, which shares it, went down as well.
Chapter 10 · Act IIIStalls That Carry Their Own Stock#
The problem
On an ordinary day Quill answers about forty price and stock lookups a second. Kofi's rehearsal of the joint sale sent two thousand requests a second for two hours, a quarter of them changes to a basket. The database begins to queue at about six hundred. Four minutes in, connections ran out, and order entry, which shares that database, stopped with it. Adding application servers changed nothing, because every one of them was waiting on the same database. The sale was three weeks away.
Ingrid proposed the familiar remedy: a read-only copy of the database and a cache in front of it. Marcus proposed taking the database out of the request path altogether, with the data held in memory beside the code.
Ana had both rehearsed. With Ingrid's design the lookups were served comfortably. The five hundred basket changes a second were not: each is a write, a cache does nothing for writes, and order entry still slowed to a crawl. With a prototype of Marcus's, the database saw a gentle, steady trickle and order entry did not notice the sale at all.
The idea
The space-based style is market stalls that each carry their own stock, and settle with the warehouse afterwards. Its one move is to remove the central database from the path of a request.
The parts. A processing unit is the application code together with an in-memory copy of the data it works on; there are many identical units, started and stopped as the load changes. An in-memory data grid keeps the units' copies in step by passing each change from one unit to the others. A data pump is a channel that carries changes away from the units. A data writer takes changes from the pump and writes them to the database. A data reader loads data from the database into a unit that is starting cold.
How they talk. A request goes to any unit and is answered from memory. A change is made in that unit's memory, copied to the other units by the grid, and sent down the pump. The database hears about it some seconds later, in batches.
Where the data lives. In memory, in every unit, and for the moment that is the working truth. The database is the long-term record, a little behind. It is what a cold unit is filled from and what the nightly line and the invoices read.
Strong and weak. Strong on elasticity and performance: add units, and each answers from its own memory with no shared queue. Fault tolerance is good on the request path, since losing a unit loses no data the others do not hold. It is weak on simplicity and testability, because the behaviour depends on load and on how fast copies converge, and neither is easy to reproduce. It is weak on cost: the working data is held in memory many times over. And it is wrong for data that must agree at every instant, because two units can change the same thing in the same second.
The cost of leaving. High. Code written against memory that the grid copies around is not code written against a database, and moving in either direction means rewriting how the application reads and writes.
It is often mistaken for something much cheaper:
| A cache in front of a database | Space-based | |
|---|---|---|
| The database is | In the path of every write and every miss | Out of the request path |
| Memory holds | Copies of some answers, which may be dropped | The whole working set |
| A write goes | To the database, then the cache is corrected | To memory, then to the database later |
| The limit on load | How fast the database can write | Memory, and how fast units stay in step |
| If memory is lost | Slower, still correct | Refilled from the database, without what had not yet been pumped |
| Cost | Low | High |
Caching and read copies belong to the system design book, chapters 9 and 5, and they are the first thing to try. Ingrid's premise was that the crowd would mostly be reading, and for a crowd that only reads she is right and the left-hand column is enough. This crowd also wrote five hundred times a second.
The rule that keeps the style safe is to put in the units only data that can be briefly out of step. A basket belongs to one customer, so two units rarely disagree about it, and a supplier's cost changes once a night. The last pallet of a sale item is different: two units can each sell it. So the stalls take lookups and baskets, and the moment of purchase, the stock decrement and the credit check, remains a call to Ordering against the database. That keeps the purchase correct, and it means checkout is the one step that cannot go faster than the database. The rule can be relaxed where a collision is rare and cheap to apologise for, a back-ordered box of pencils, and the apology is the cost.
The style has a relative. In the actor model, a program is many small actors, each with private state, that affect each other only by sending messages and share no memory at all. Platforms built on it reach the same end by the same instinct: keep state next to the code that uses it, and let nothing central sit in the path.
Remember it as: each stall sells from its own stock and settles with the warehouse later: fast for crowds, wrong for the last item on the shelf.
What Ana learned
- Measure the peak and what it is made of before choosing, because copies can serve the reads and it is the writes that decide.
- Put in the units only data that can be briefly out of step, and keep what must agree behind a call.
- Check every day that the data writer is keeping up, because the database is still the record.
…but
The sale held. In October finance sent Ana the bill. The units had been scaled down after the sale, but the grid's minimum of eight and its licence are paid for all year round, for a peak that comes on six days. The same week, the team that converts uploaded order spreadsheets asked for four more servers, for a job that is busy from eight to ten on Monday mornings and close to idle the rest of the week.
Chapter 11 · Act IIIRenting by the Hour#
The problem
Six in ten of Quill's orders arrive as an uploaded spreadsheet, and a converter checks each one and turns it into an order. On a Monday about 500 arrive between eight and ten. For the rest of the week there are a few dozen an hour, and at night and at weekends there are none.
The converter runs on two servers. They are idle for most of the week and not enough for Monday, when the queue now lasts until noon. A primary school's order for the first day of term sat in that queue until 11:40, missed the dispatch cut-off and arrived a day late. The team asked for four more servers, which would also be idle for most of the week.
Finance had just queried the grid from the last chapter on the same grounds: money spent on machines that mostly wait.
Marcus proposed functions: code that the cloud provider runs when a file arrives and stops when it has finished, billed by the second. Ingrid had two objections. Every function is written against one provider's storage, queues and permissions, so leaving means rewriting. And a function that has not run for a while takes time to wake.
The idea
Serverless is renting the hall by the hour: no rent when it is empty, and a wait while the lights come on. The servers are the provider's, and you do not pay for them while your code is idle.
The parts. Functions and managed services. A function is a small piece of code that the provider starts on demand and stops when idle; this half is called function as a service. The other half is managed back-end services: file storage, queues, databases and sign-in that the provider operates and you only configure. Triggers join them: a file arrives, a message arrives, a request arrives.
How they talk. Through triggers and the managed services, almost always asynchronously. A file landing in storage starts one function; that function leaves a message on a queue; the message starts the next. Functions do not hold connections to each other.
Where the data lives. Never in a function, which remembers nothing from one run to the next. It lives in the managed stores.
Strong and weak. Strongest of any style on elasticity: from nothing to a thousand copies and back with nobody doing anything. Cost is good for work that comes in bursts. Deployability is good, since each function ships alone, and fault tolerance is good, since the provider restarts and retries. Performance is weak where a person is waiting, because of the cold start. Testability is weak, because the behaviour is mostly the provider's services and they do not run on a laptop. Simplicity is fair at best: each piece is trivial and the wiring between them is not.
The cost of leaving. The code inside a function moves easily. The triggers, permissions, queues and stores around it do not, and they are most of the system. That is lock-in, and it is a trade-off like any other: the price of leaving, multiplied by the chance of having to, against what the provider's services save every day. Avoiding it has a price too, usually a home-made layer that hides the provider and uses none of its services well.
A cold start is the wait while the lights come on. When a function has not run recently the provider must start a fresh copy, which takes from a few tenths of a second to several seconds. A spreadsheet conversion that runs for twenty seconds does not care. A customer pressing a button does. So the rule is to keep cold starts off any path a person waits on. Providers will keep copies warm for a fee, which is a legitimate way to break the rule, and it means paying rent on an empty hall, the thing the style was chosen to avoid.
The cost has a shape worth drawing on any whiteboard. Pay-per-use starts at nothing and rises in a straight line with use. A server is a flat line: the same whether busy or idle. The lines cross, and the crossover is the number to know. Kofi worked it out. At the prices Bramble pays, one conversion costs about a fortieth of a penny in function time, and a server costs £90 a month. Twelve thousand conversions a month come to about £3. The crossover is near 350,000 a month, one every eight seconds around the clock. The converter is far below it. Quill's price lookups, at forty a second all day, are far above it and have a person waiting, so they stay where they are.
| Microservices | Serverless functions | |
|---|---|---|
| The unit | A service: one capability, always running | A function: one action, running only when triggered |
| You operate | The service, its runtime and its scaling | The code and its configuration |
| Kept between requests | Connections, caches | Nothing |
| Billed for | Capacity, busy or idle | Each run |
| Start-up delay | When deployed | Possibly on any request |
| Tied to | A container platform, fairly portable | One provider's triggers and services |
The converter moved to a function, and so did invoice rendering at month end and the receivers for partners' notifications. On Monday every upload gets its own copy, and the queue is gone. Marcus was right about all three. Ingrid was right that they are now tied to the provider, so Ana had the cost of leaving estimated, at about two weeks for all of them, and wrote it beside the decision. Her premise, that the tie is too dear, would hold for a system that was large, central and likely to move.
Remember it as: rent by the hour what is usually idle, own what is always busy, and keep the wait for the lights off the critical path.
What Ana learned
- Plot renting against owning before moving anything, and move what sits well below the crossover.
- Keep whatever a customer waits for away from a cold start, or pay for a warm copy and say so.
- Estimate the cost of leaving in days, and write it beside the decision.
…but
Encouraged, the upload team split the converter into eleven functions: one to unzip, one to detect the format, one for each validation rule, joined by seven queues. An upload that fails now fails somewhere among them, and nobody can say where. Marcus's draft goes the same way on principle, with nothing larger than could be rewritten in two weeks. It divides the Ordering service into fourteen.
Chapter 12 · Act IIIHow Small Is Small Enough?#
The problem
The eleven functions lasted three weeks. Then a school's upload vanished between two of them, and it took a day to find the queue it was sitting in.
Marcus's draft would do the same to the Ordering service, dividing it into fourteen: order capture, validation, credit check, amendment, cancellation and nine more. His case is that a small service is easy to understand, easy to hand to one team and easy to replace. Ingrid's case for keeping four services was that they work. One said "too big" and the other said "too many", and neither could say what size would be right.
There was already one piece of evidence. Last quarter a team had moved the credit check out of Ordering into a service of its own, with its own table of credit limits. Placing an order then meant two things had to happen in two places: Ordering saves the order, and Credit reduces what the account has left. One Friday afternoon the Credit service was slow. Orders were saved, the calls to reduce the limits timed out, and nothing tried again. Thirty-seven orders were accepted beyond their accounts' limits, and £61,000 of goods left the warehouse on credit that nobody had approved.
The service was small. It was easy to understand. It was the wrong size.
The idea
Granularity is how small a shop can be before the shopkeeper spends the day walking between shops. Size turns out to be a poor guide to it. What decides the right size is two sets of forces, and the question for any proposed split is which of them are present.
Disintegrators are reasons to pull a service apart:
- Scope and function: it does two things that have nothing to do with each other.
- Volatility: one part changes weekly and the rest once a year.
- Scalability and throughput: one part needs many more copies than the rest.
- Fault tolerance: one part's failures should not take the rest down.
- Security: one part handles data that needs tighter access.
- Extensibility: one part will grow by additions that are already planned.
Integrators are reasons to keep it together, or to put it back:
- Database transactions: the parts must change data together, all or nothing.
- Workflow: the parts would call each other on almost every request.
- Shared code: they would share a body of code that changes with both.
- Data relationships: their data is so interlinked that every query would cross the boundary.
The rule is that a split needs a disintegrator you can name and must survive all four integrators. The credit check had one of the first kind, scope, and ran straight into the strongest of the second: an order and the credit it uses up must change together. The rule can be broken. If the reason to split is strong enough, a transaction can be replaced by a saga, a sequence of separate steps with an undo for each, which the Atlas explains in chapter 16. The cost is that someone must then design what the database used to do for nothing.
That design has three choices in it: whether the services talk synchronously or asynchronously (communication), whether the whole must succeed or fail as one or may settle later (consistency), and whether one coordinator runs the steps or each service reacts to the last (coordination). Three choices of two make eight combinations, and the authors of Software Architecture: The Hard Parts gave each a name. This is the saga trade-off matrix:
| Saga | Communication | Consistency | Coordination | What you get |
|---|---|---|---|---|
| Epic | Synchronous | Atomic | Orchestrated | Behaves like the old transaction; tightest coupling, slowest |
| Phone Tag | Synchronous | Atomic | Choreographed | All-or-nothing with nobody in charge; error handling is hard |
| Fairy Tale | Synchronous | Eventual | Orchestrated | A coordinator, each step committing alone; the usual sound choice |
| Time Travel | Synchronous | Eventual | Choreographed | A chain of calls; fine for simple one-way flows |
| Fantasy Fiction | Asynchronous | Atomic | Orchestrated | Atomic over messages; much complexity, little gain |
| Horror Story | Asynchronous | Atomic | Choreographed | The hardest of the eight; avoid |
| Parallel | Asynchronous | Eventual | Orchestrated | Scales well, and errors still have a home |
| Anthology | Asynchronous | Eventual | Choreographed | Loosest coupling and highest throughput; hardest to follow |
The matrix is useful less for its names than because it shows there is a choice. The team that split out the credit check never made one. They wrote two synchronous calls, assumed both would succeed, and put nobody in charge of the case where one did not: a Phone Tag saga with the error handling left out.
Code that both halves need has three possible homes. A shared library is compiled into each service; it suits stable code and is chapter 4's trap for business rules that change. A shared service is one running copy that everyone calls; there is one place to change, and every caller is now down whenever it is. A sidecar is a small separate process deployed beside each service, carrying what every service needs and no business cares about: logging, security checks, traffic control. The working rule is to share operational code freely and business rules rarely; a small, stable value such as Money is the exception worth a library.
Follow the disintegrators all the way and you reach the microservices style: a street of small shops, each with its own till and stockroom. Each service is one business capability with its own data, its own build and release, and usually its own team, and the services deal with each other as equals by calls and events. It is the strongest style for deployability, elasticity, fault tolerance and evolvability, and among the weakest for simplicity and cost, because every integrator that was overridden has become a network call, a copy of data or a saga. Testing is only fair: each part is easy to test and the whole is not. The Atlas defines the style in its chapter 2 and prices it in chapter 21, and its row is in the next chapter's table.
Quill's credit check went back into Ordering, where an order and its credit change in one transaction. The eleven upload functions became three. Ingrid was right about thirteen of Marcus's fourteen. He was right about one: taking in uploaded orders changes weekly as customers' spreadsheets change, arrives in bursts, and hands over a finished order without sharing a transaction. It has two disintegrators and no integrator, and it stays outside Ordering, as the functions of chapter 11.
Remember it as: split a service only when a named force pulls it apart and no stronger force holds it together.
What Ana learned
- Ask for the named force behind every proposed split, and test it against the four that hold things together.
- Keep in one service whatever must change in one transaction, unless you are prepared to design the saga on purpose.
- Share operational code freely and business rules rarely.
…but
Marcus's final report arrived on the Friday, and its recommendation fitted on one page: a single target architecture for the whole merged company, event-driven microservices on one platform, for order entry, the nightly line, the hall and the storefront alike. Lena liked that it fitted on one slide. She asked Ana for her objection, on one slide as well.
Chapter 13 · Act IVNot One Plan for the Whole Town#
The problem
Marcus's target architecture was one diagram with the word all in its title: event-driven microservices on a single platform, for every system in both companies. His argument was a serious one. One style means one set of skills to hire for, one platform to run, and engineers who can move between teams without relearning their trade.
Ana could not answer that with a preference, and did not have to. Lena wanted evidence before the board met, so Marcus's team rebuilt one system "to the standard" as a proof of concept: the nightly line of chapter 6. Fourteen stations became six services passing nine million events through the broker. It ran beside the old line for three weeks, and Kofi kept the figures. It took over six hours where the line takes four, and cost four times as much to run. Re-running a night meant replaying the events and took most of a day. On the second Monday it missed six o'clock. The pilot was switched off and the line was kept.
The idea
Choosing a style is zoning: a factory, a school and a row of houses do not share one plan. The choice starts from the system's three driving characteristics, never from a style.
The first question is monolithic or distributed. If every part of the system needs the same things, the system is one quantum, and a monolithic style will be cheaper, simpler and easier to test. If some part must scale, deploy or fail separately from the rest, there is more than one quantum, and the network is a price worth paying. From this follows the rule that Marcus's slide broke: choose one style per quantum, not one per company. A company's systems have different drivers, and a style that serves one set will fail another.
The tree gives candidates. The comparison table shortlists among them, and the trade-off session of chapter 3 decides. For cost, Good means cheap.
| Style | Deployability | Elasticity | Fault tolerance | Performance | Simplicity | Testability | Evolvability | Cost |
|---|---|---|---|---|---|---|---|---|
| Layered | Poor: every change ships everything | Poor: scales only as a whole | Poor: one process, one failure | Fair: in-process calls, one database | Good: one program, familiar to everyone | Fair: layers stubbed, changes cross them | Poor: business changes cross every layer | Good: one build, one thing running |
| Modular monolith | Poor: one release for every module | Poor: scales only as a whole | Poor: one process, one failure | Fair: in-process calls, one database | Good: one program, enforced borders | Good: a module tests alone | Good: business change stays in module | Good: one build, one thing running |
| Pipeline | Poor: the whole line ships together | Poor: stations scale together | Fair: restartable only if designed | Fair: sequential, suits batches | Good: one direction, one contract | Good: file in, file out | Good: add or swap a station | Good: cheap to build and run |
| Microkernel | Fair: stalls can ship without core | Poor: everything under one roof | Poor: a bad plug-in hurts all | Fair: in-process calls through contract | Good: small core, uniform plug-ins | Good: plug-in tested against contract | Good: add a plug-in, touch nothing | Good: one deployable to run |
| Service-based | Good: each team ships alone | Fair: coarse services, shared database | Fair: services isolated, database shared | Fair: few network hops | Fair: simplest of the distributed styles | Good: one business area per service | Good: cut along business lines | Fair: few services, data unmoved |
| Event-driven | Good: processors ship alone | Good: scale each by queue length | Good: notices wait for slow readers | Good: nobody waits for anybody | Poor: the flow is written nowhere | Poor: behaviour depends on timing | Good: new reader, poster unchanged | Fair: a broker and its upkeep |
| Space-based | Fair: many identical units, one grid | Good: add units, no shared queue | Good: every unit holds a copy | Good: answers come from memory | Poor: replication, pumps and collisions | Poor: needs load to reproduce | Fair: code is built around grid | Poor: data in memory many times |
| Serverless | Good: each function ships alone | Good: zero to thousands, unattended | Good: provider restarts and retries | Poor: cold starts, limits per run | Fair: trivial pieces, intricate wiring | Poor: provider's services not on laptops | Fair: tied to one provider's services | Good: nothing while idle; watch crossover |
| Microservices | Good: every service ships alone | Good: scale one capability alone | Good: failures stay in one service | Fair: every interaction is a hop | Poor: sagas, contracts, many moving parts | Fair: parts easy, the whole hard | Good: small parts replaced independently | Poor: a build and pager each |
These ratings are this book's judgement, taken from chapters 5 to 12 where those chapters speak and reasoned the same way where they are silent. They follow the star ratings in Richards and Ford (2020) in their broad pattern: monolithic styles are cheap and simple, distributed ones elastic and costly. They differ in using three levels where that book uses five, and in adding the modular monolith and serverless, which its 2020 catalogue does not rate.
Read the pilot against the table. The nightly line must finish by six, be re-runnable and be cheap to change. The pipeline row is good on simplicity, testability and cost. The event-driven and microservices rows are good on elasticity and deployability, which the line does not need, and poor on simplicity, which it does.
Styles also combine, and a hybrid is normal. Quill's back office is service-based, its services talk by events, the nightly line beside it is a pipeline, Dispatch contains a hall, and order intake is rented by the hour. The rule is one style for each quantum at the top level, with others nested inside where a part has a different shape. Its limit is the one Marcus named: each style in use is another thing to staff and to run, so a hybrid should be chosen and never drift into being.
That is also where his argument holds. Standardising on one style is the right call when a company's systems have much the same drivers, when it is small enough that one platform is all it can operate, or when people move between teams so often that familiarity is worth more than fit. And one layer should be standard everywhere: the platform beneath the systems, how they are deployed, observed and secured. Standardise the roads. The buildings follow what each must survive.
Remember it as: zone the town: choose each system's style from what that system must survive, and standardise the roads, not the buildings.
What Ana learned
- Start every choice of style from the system's three drivers, and ask first whether all of its parts need the same things.
- Use the table to shortlist and the trade-off session to decide.
- Standardise the platform and the way of working, and let each system's shape follow its drivers.
…but
Three weeks after the styles were agreed, a new engineering manager, hired from a company that ran on microservices, asked in a planning meeting why Quill's back office shares one database "against best practice", and proposed dividing it. Ingrid was on holiday. Nobody else in the room could remember the argument well enough to answer him.
Chapter 14 · Act IVWrite It Down#
The problem
The decision to keep one database behind the back-office services was made in chapter 8, by four people in a room, after an afternoon's argument. It was a good decision, and it left no trace.
So when the new engineering manager asked why, nobody could tell him. Ingrid was away. Ana remembered the conclusion and not the reasoning. His proposal sounded sensible, and a team began giving Invoicing a database of its own. Two weeks in, they reached month-end reconciliation, the one query that joins orders, invoices, payments and credit notes, and stopped. Three people's fortnight was written off and the work was reversed.
Then came the meeting about the reversal. Ingrid was back. For ninety minutes she, Marcus, the manager and Ana had the argument of chapter 8 again, from the beginning, and reached the same conclusion. It was the third time the decision had been made, and afterwards there was still nothing to show for it but four people's memories.
Marcus's remedy was a proper architecture document: every view of the system and every decision, in one maintained volume. Ingrid's was the opposite. She had never needed a document. The decisions were in the code, and she was there to explain them.
The idea
An architecture decision record, ADR for short, is the planning file: what was decided, when, what was known, and what it cost. It is one decision on about one page, in plain text, numbered, and kept in the repository beside the code it governs. It has five parts.
- Title: the decision as a short phrase.
- Status: proposed, accepted, or superseded, with dates.
- Context: what was true and what mattered when the decision was made. This is the part memory loses first.
- Decision: one or two sentences beginning "We will".
- Consequences: what becomes easier and what becomes harder. A record that names no cost has not finished thinking.
Bramble adds a sixth line, Review when: the evidence that would reopen the decision.
Here is the record that should have existed in chapter 8:
ADR 7: The back-office services share one database
Status: Accepted, 26 June. Superseded in part by ADR 19.
Context: Three teams could not ship separately from one release. The failing characteristic was deployability, not scale or resilience. The database has 400 tables. Month-end reconciliation is one query across orders, invoices, payments and credit notes, and finance must be able to reproduce any invoice to the penny.
Decision: We will split the back office into four services (Ordering, Pricing, Invoicing, Dispatch) and keep one database, with a schema for each service and exactly one service writing to each schema. Reads across schemas are allowed by agreement and are listed in this repository.
Consequences: Each team ships on its own day. Reconciliation and reporting stay as single queries. Harder: a column that another service reads cannot be renamed without that service. All four services slow down or fail together if the database does. No service can be scaled or secured separately at the level of its data.
Review when: a service needs its own scaling or its own security boundary; or schema changes force more than one service to deploy more than twice in a quarter.
With that in the repository, the manager's question takes two minutes: read ADR 7. His proposal is still welcome, but it now has a form. It is a new record, and its context must say what has changed since the old one.
Something did change, in the new year. Lena's third request was to offer Quill's invoicing to Bramble's marketplace sellers, so that they could invoice their own business customers through it.
ADR 19: Invoicing gets its own database
Status: Accepted, 13 January. Supersedes ADR 7 for the Invoicing service only.
Context: Invoicing will be offered to Bramble's marketplace sellers. Their invoices belong to other companies and must be kept apart from Quill's data, and volume is expected to rise twentyfold. Both are review triggers named in ADR 7. Nothing has changed for Ordering, Pricing or Dispatch.
Decision: We will move the invoicing schema to a database owned by the Invoicing service. Month-end reconciliation will read invoices from a copy that Invoicing publishes each night.
Consequences: Invoicing can be secured and scaled alone. Harder: reconciliation now depends on a nightly copy and is up to a day behind. Orders and invoices can no longer change in one transaction, so "mark as invoiced" becomes an event. There is one more database to run and back up.
Review when: reconciliation needs figures less than a day old; or the sellers' volume stays under a tenth of the forecast for two quarters.
This is superseding, and it is the only way an accepted record changes. ADR 7's text is left exactly as it was written; only its status line gains a pointer to its successor. The reason is that the old record is still true as history: it says what was known in June. The rule has an obvious limit: a typing error may be corrected. It also has a cost: a reader must follow the chain, which is why the pointers run both ways.
The records in number order are the decision log. A rejected proposal stays in it, because "we considered that in March, and here is why not" is half of what the log is for.
Not every decision earns a record. The test is chapter 1's: would reversing it take more than one team and one month? A log with a page for every library choice hides its walls among its furniture.
Two other documents are often confused with the record, and a large decision may properly have all three.
| Decision record | Design document | Request for comments | |
|---|---|---|---|
| Answers | What did we decide, and why? | How will we build it? | What do you think of this proposal? |
| Written | When the decision is made | Before building | Before deciding |
| Length | About a page | Many pages | Any |
| Afterwards | Never edited, only superseded | Stale once the thing is built | Closed; its outcome becomes a decision record |
| Kept | With the code, for good | With the project | Wherever the discussion was held |
Ingrid was right that Marcus's volume would not have been read; large documents are not, and they are out of date within a quarter. His premise, that someone will keep it current, holds where documentation is a contractual deliverable, as in regulated or safety-critical work. Marcus was right that a decision held in one head is not recorded. Ingrid's premise, that she is there to explain, holds for a team small enough to ask her and a system she never leaves. It failed for the length of one holiday and cost three people a fortnight.
Remember it as: one page for each decision: what we decided, what we knew, what it costs, and what would make us think again.
What Ana learned
- Write one page for each decision that fails the reversal test, on the day it is made, and keep it beside the code.
- Refuse to accept a record whose consequences name nothing that became harder.
- Reopen a decision only with a new record that says what has changed.
…but
The log now holds nineteen records. A developer who joined in January read ADR 7 and asked which box "the back office" was. Three teams sent her three diagrams. One had four boxes, one had nineteen, and one had a cylinder labelled "Quill". At the meeting to settle it, two people argued about an arrow for ten minutes before finding that one of them meant a service by a box and the other meant a class.
Chapter 15 · Act IVDraw It So They Can Read It#
The problem
Each of the three diagrams was accurate, to the person who drew it. The four-box one showed the services. The nineteen-box one showed services, modules and two database tables, all as rectangles. The cylinder labelled "Quill" was how the storefront team thought of the whole thing. None of them said what a box stood for, what an arrow meant, or how far away the viewer was standing.
The new developer had picked the nineteen-box diagram, because it had the most detail. On it was a box called Reconciliation, with arrows in and out, looking like every other box. She spent three weeks building an integration that called it. Reconciliation is a module inside the Invoicing service. It cannot be called from anywhere. Her work was thrown away.
Marcus's answer was a standard. There is a formal notation with defined meanings for every shape and line, taught for decades; adopt it and a diagram means the same to everyone trained to read it. Ingrid's answer was that she had run Quill for eleven years on whiteboard sketches, drawn in conversation and wiped afterwards, and that formal models are out of date before they are finished.
The idea
The C4 model is a set of map scales: the region, the town, the building, the room. Nobody tries to show a country and a floor plan on one sheet. C4 gives software four sheets, each answering a different question for a different reader.
| Level | Shows | A box is | Drawn for |
|---|---|---|---|
| Context | The system, its users and the systems around it | A whole system | Everyone, including people who do not write code |
| Container | What runs inside the system and how the pieces talk | Something separately runnable: a service, an application, a database, a queue | Whoever builds or operates it |
| Component | The parts inside one container | A module with one job | The team that owns that container |
| Code | Classes inside one component | A class or a function | Almost nobody; generate it when needed |
"Container" is C4's word for anything that runs or stores data on its own, and has nothing to do with any packaging technology of the same name.
Here is the Quill back office at the first three scales. The context diagram is the one to show Lena.
Zoom in on that one box and it becomes the container diagram, which is the one the new developer needed.
Zoom in once more, on Ordering alone, and the Ordering team has its component diagram.
The levels are half of the model. The other half is four notation rules, and they are the rules this book's diagrams have followed since chapter 1; go back and check.
- State the level. Each structure diagram has a caption that begins with it.
- Label every arrow with a verb, and between containers with the protocol or medium. An unlabelled line says only that two things are somehow related.
- One level of abstraction on one diagram. A class never stands beside a service.
- Give a legend. Say what each shape, colour and line style means. In this book a solid arrow is a call that waits for its answer, a dashed arrow is asynchronous, and a frame around several boxes is one deployable; the full key is in the epilogue.
The rules have a limit. A sketch on a whiteboard in the middle of a conversation needs none of them, because its author is standing beside it answering questions. They apply from the moment a diagram is saved where someone can find it without its author. Skipping them then costs what it cost here: three weeks of one person's work.
How a diagram is kept matters as much as how it is drawn. Every diagram in this book is a few lines of text, stored beside the chapter that uses it and changed in the same commit. That is diagrams as code: the diagram is reviewed in the same pull request as the change it describes, its history can be read, and nobody needs a drawing tool that only one colleague owns. It does not make a diagram true, which is the next chapter's subject, but it removes the usual reasons for letting one go stale.
C4 is not the only way to organise views, and two others are worth knowing by name. Philippe Kruchten's 4+1 model describes a system through four views (logical, process, development and physical) tied together by a fifth, a handful of scenarios. arc42 is a template for a whole architecture document, in which diagrams like these fill some of the sections.
Bramble now keeps a context and a container diagram for every system, as text, beside its decision log, and a component diagram only where a team wants one. Marcus was right that a notation must have agreed meanings. His premise, that a formal standard is the way to get them, holds where models are exchanged between companies or used to generate code; it fails where most readers were never taught the standard. Ingrid was right that a sketch is enough while its author is in the room, and the new developer is what happens when she is not.
Remember it as: draw one scale at a time, say which, label every arrow and give the key; a map without a scale is only a picture.
What Ana learned
- Draw a context and a container diagram for every system, state the level and label every arrow.
- Keep diagrams as text beside the code, so that they are reviewed and changed with it.
- Give each audience the scale it can use, and stop drawing at the scale nobody reads.
…but
The records and the diagrams now agree, and both are in the repository. Two weeks later an ordinary pull request in Ordering added a query that reads Invoicing's tables directly, to save a call. Two reviewers approved it. The container diagram still shows no such arrow.
Chapter 16 · Act IVKeep It True#
The problem
The order history screen needed to show whether each order had been invoiced. The developer had two proper ways to find out: the copy that Ordering builds from Invoicing's events, which runs a few seconds behind, or a call to the Invoicing service. A third way was quicker. Invoicing's own data-access classes were still within Ordering's reach, left over from before the split, and an old configuration file still held a password for Invoicing's database. The screen could simply use them to read the invoice table.
The pull request was forty lines. The tests passed. Two reviewers approved it, and neither had ADR 19 in mind.
Ten days later the Invoicing team renamed a column, in their own database, for the sellers' launch. At 09:20 on a Tuesday the order history screen began to fail for every customer, and it stayed down for fifty minutes while the Invoicing team tried to understand how a change that touched nobody else's code had broken somebody else's screen.
The rule existed. It was written in a record and drawn on a diagram. Nobody had broken it on purpose. It depended on every reviewer remembering it on every pull request, and on one ordinary afternoon they did not.
Marcus drew the larger conclusion: an old system patched this way will keep doing this, so build the target clean and retire what is there. Ingrid drew the opposite one: nobody who rewrites a working system from nothing gets back what they threw away.
The idea
A fitness function is the building inspection, repeated on every change. It is any automated check that reports whether the system still has a characteristic it was designed for, or still obeys a rule it was designed around. Two distinctions sort them:
- Atomic or holistic. An atomic check looks at one thing in isolation: Ordering uses no other service's internals. A holistic one looks at several things together, usually on a running system: with the nightly line running, order entry still answers within a second.
- Triggered or continual. A triggered check runs when something happens: a commit, a build, a deployment. A continual one runs all the time in production: how far the data writer of chapter 10 has fallen behind.
The cheapest and most useful kind is atomic and triggered: an architecture test in the build. It reads the code's own dependencies and fails if a forbidden one exists.
public final class ServiceBoundaryCheck {
public record Dependency(String fromClass, String toClass) {}
/** Fails the build if a service uses another service's internals. */
public static void check(String service, List<Dependency> dependencies) {
String own = "quill." + service + ".";
var violations = dependencies.stream()
.filter(d -> d.fromClass().startsWith(own))
.filter(d -> !d.toClass().startsWith(own))
.filter(d -> d.toClass().contains(".internal."))
.toList();
if (!violations.isEmpty())
throw new AssertionError(service
+ " reaches into another service's internals, against ADR 19: "
+ violations);
}
}
The list of dependencies comes from a tool that reads the compiled classes. Architecture-test libraries that do this exist for every major language and run inside the ordinary unit-test run, so a violation stops the same build a failing test would, with a message that names the record. The developer of the order history screen would have met ADR 19 on a laptop, in the first minute.
Better than detecting the wrong thing is making it impossible. Each service's database account now sees only its own data, so the forbidden query fails wherever it is run. Permissions are fitness functions too, of the continual kind.
A fitness function looks like a test and is a different thing:
| Test | Fitness function | |
|---|---|---|
| Protects | What the system does | A characteristic or a structural rule |
| Fails when | A feature is wrong | The architecture has moved |
| Written from | A requirement | A decision record or a scenario |
| Example | An order over its credit limit is refused | Ordering uses nothing inside Invoicing; the nightly run ends by 06:00 |
| Runs | In the build | In the build, at deployment, or continually in production |
The rule is that every architectural rule gets a check, and the record says which. Its limit is the rule that cannot be checked cheaply by a machine, such as "every reader of this event is listed". That one becomes a named person's review on a set date, and it will decay faster than the automated kind, which is the cost.
What the checks hold back has two names. Erosion is the breaking of a stated rule, as the pull request did. Drift is the extension nobody applied for: change that breaks no rule, because nobody thought to make one, and still moves the system away from its design. The reports that bypassed the business layer in chapter 5 were drift.
Shortcuts taken along the way are technical debt: deferred maintenance, with interest. In Martin Fowler's terms (2009) it is deliberate when the team knows it is borrowing, as in "read the table directly for this release, and replace it by March", and inadvertent when nobody knew, which is the usual case and the worse one. Deliberate debt is a legitimate tool if it is written in a register with what it costs each month and what will trigger repayment. The forbidden query was debt that nobody knew they owed.
With records, diagrams and checks in place, an architecture can be changed in small steps without losing its shape, and that is evolutionary architecture: leaving the gable wall plain because a wing will be added there. Here the town stops being a good guide, and the book should say so. A building cannot be rebuilt every week. Software can be copied, replaced and rebuilt cheaply and repeatedly, and that difference is the whole case for evolving a system under inspection instead of planning it once like a cathedral, or demolishing it and starting again.
The same visibility is wanted for technology choices, and the usual instrument is a technology radar: a short published list, revised a few times a year, sorting each technology into adopt, trial, assess or hold. On Bramble's, the message broker is adopt, the data grid is trial, and reading another service's database is hold.
Ingrid was right about the back office. It is being repaired where it stands, one check at a time, and the fifteen years of pricing rules in it are not being rediscovered the hard way. Marcus was right about a smaller thing: the first upload converter was thrown away and rebuilt as functions, and nobody mourned it. His premise, that replacing is cheaper than repairing, holds for a system small enough to rewrite in weeks and understood well enough to rewrite correctly.
Remember it as: a rule that nothing checks is a wish; put the inspection in the build, and it happens on every change.
What Ana learned
- Give every rule in a decision record a check that fails the build, and name the record in its message.
- Treat a red check as a stopped line, and delete any check you are not prepared to obey.
- Keep a register of debt with its monthly cost, and repay it in small steps under the checks.
…but
Forty-one checks now run on every change, and they are green. But every exception to one, every new record and every "are we allowed to?" comes to Ana. Her review calendar is full for three weeks ahead. A team that needed a queue waited eleven days for her approval, then added it without telling her.
Chapter 17 · Act IVThe Architect in the Room#
The problem
Every exception to a check, every new decision record and every question that begins "are we allowed to" now goes to Ana. There are nine teams. Her reviews are booked three weeks ahead.
One team needed a queue between two of its own components. It asked for approval, waited eleven days, and then added the queue without telling her. Nobody was told, so nobody watched it. Three weeks later it filled overnight, and two thousand dispatch notifications sat in it until morning.
A second incident bothered her more. A team proposed the cloud provider's managed queue, and Ana said no, because in 2019 she had lost messages on a managed queue and spent a weekend finding them. The team's engineer showed her, politely, that it had been a different product with a different guarantee, six years ago. She had not built anything on either since.
Ingrid recognised the first problem and not the second. For eleven years she had made every significant decision at Quill herself, and the system had one mind behind it, which showed in its consistency. But Quill had twelve developers, and she had written code alongside them every week.
The idea
The architect is the town planner, who walks the streets and also sits in the council meeting. The earlier chapters were about decisions. This one is about how decisions get made, which is itself a set of decisions, with trade-offs like any other.
Breadth over depth. What anyone knows falls into three parts: what they know well, what they know exists, and what they have never heard of. A developer is valued for the first, depth. An architect is valued for the second, breadth: knowing that five ways of doing something exist, and when each one fits, is worth more to a decision than mastery of one. Nobody can keep depth in everything, and an architect who tries ends up defending expertise that has gone stale, which is what Ana did with the queue. The rule has a limit. With no depth at all, an architect cannot judge what things cost, so most keep their hands in somewhere, and pay for it in time.
The architect elevator. A large organisation is a tall building: strategy is set in the penthouse and systems are built in the engine room, and the floors between speak different languages. The architect's distinctive work is riding the elevator: telling Lena what "one company within a year" costs in release trains and databases, and telling the teams why invoicing for sellers matters enough to split one off. An architect who stays on the top floor produces slides, and one who stays at the bottom is a senior developer with a grander title.
How much control. There is no fixed right amount. Richards and Ford (2020) list what it depends on: how well the team's members know each other, the team's size, its experience, the complexity of the work and how long it will last. A large, newly formed team on a long and complicated project needs an architect close by and firm. A small team that has worked together for years needs to be left alone. The same architect should behave differently in the two rooms, and should loosen the grip as a team learns.
The architecture advice process. This replaces approval. It is one rule with one condition: anyone may take an architectural decision, provided that before deciding they seek advice from everyone the decision will meaningfully affect and from people with expertise in the matter. In the town: anyone may build, after asking the neighbours and the planner, and filing the plan. They are not obliged to follow the advice. They are obliged to seek it, to write down what they were told, and to record the decision, in the form of chapter 14. A weekly advisory forum, an hour long and open to all, is where proposals are aired and where everyone hears the advice given to everyone else.
The architect is in this picture as an adviser, one voice among those with expertise, and no arrow in it is labelled "approves". That is the change. The rule has a limit as well: a one-way door that binds every team, such as the choice of platform or the security baseline, keeps a named owner, and is slower for it.
Principles and the paved road. People deciding for themselves need to know what the organisation cares about. A short list of principles says so, each drawn from a decision already in the log: every table has one writing service; a customer never waits on a courier. And a paved road makes the right way the easy way: a supported platform, and project templates with the fitness functions already wired in. A team that stays on the road needs nobody's advice. A team that leaves it must seek advice, and say why in a record.
Under the new arrangement the team with the queue would have decided within two days, with operations in the conversation and an alarm on the queue. Ana's calendar emptied. She now spends one day a week working inside a team, a different one each month, which is where a planner learns what the streets are like.
Ingrid's way was right for Quill as it was. One mind can hold a system built by twelve people, and the consistency that results is real. It stops working when the one mind becomes the queue. What survives of it is the list of principles, which is conceptual integrity written down so that nine teams can share it.
Remember it as: anyone may decide, after asking those affected and those who know, and writing it down; the architect advises and keeps the roads paved.
What Ana learned
- Stop approving and start advising: let teams decide once they have sought advice, and insist on the record.
- Publish the principles and pave the road, so that the easy way is the right way and needs no meeting.
- Spend the time this frees inside the teams, because advice from someone who has built nothing lately is the caveman's.
…but
Lena has booked the board review for March, a year to the week after the acquisition. She wants to be shown the architecture of the whole company, and she would still like it on one slide.
Chapter 18 · Act IVOne Town, Three Plans#
The problem
The board meets in March, a year to the week after Bramble bought Quill, and Lena has to tell it what the company's architecture is and why it should go on paying for it.
She has two slides to choose from. Marcus's is the one from chapter 13: one style, one diagram, the word all. It is easy to present. Ana's has three rows on it, which looks, at a glance, like three times the trouble.
The board will also ask about the year's three most expensive weeks, because finance has itemised them. A pilot migration that was built, run for three weeks and switched off. A database split that was started, found to break month-end and reversed. Fifty minutes with no order history, caused by a pull request that two people approved. Lena wants to be able to say that these will not happen again, and why.
Ana noticed what the three had in common. The pilot was a style chosen without the system's drivers. The split was a decision reopened without its record. The outage was a rule without a check. Each was a different missing piece of the same practice.
The idea
An architecture review is that practice, done in public for a whole system or a whole company. It is a fixed list of questions, asked of every proposal, including the reviewer's own:
- What are the systems, meaning the parts with different needs?
- For each, what are its three driving characteristics, as scenarios with numbers?
- Which style serves them, and what does that style give up?
- Where is each significant decision recorded?
- Which check keeps each rule true?
- What evidence would trigger a review of each decision?
A review that skips the second question is a discussion of taste. One that skips the sixth produces decisions that are either never looked at again or reopened whenever someone new dislikes them. The rule is that every recorded decision names its trigger. A two-way door does not need one, since it can simply be changed, and writing triggers for furniture is the same waste as writing records for it.
This is Ana's slide.
| System | Must survive | Style | Gives up |
|---|---|---|---|
| The nightly line | Finished by six; safe to re-run; every invoice reproducible | Pipeline | Prices that change within the day |
| The back office | Each team ships alone; orders and credit correct; cheap to run | Service-based, with events after the order | Separate scaling; one database is one fate |
| The sale-day storefront | Thousands of requests a second, on six days a year | Space-based, with checkout as a call | Simplicity and money; baskets briefly out of step |
Two smaller pieces complete the picture: the partner integrations, a hall with stalls inside the back office's Dispatch service, and the intake of uploaded orders, rented by the hour. Under all of it is one platform, one message broker and one paved road.
At the rehearsal, Ana asked the questions this book has been collecting, of her own rows first, with Marcus and Ingrid in the room.
From chapter 6, of the nightly line: "If station seven fails at 03:10, which stations run again, and what is written twice?" Station seven and those after it; nothing, because each replaces its own output.
From chapter 13, of the back office: "Which part of this system must scale, deploy or fail separately from the rest?" Invoicing, since the sellers arrived, and ADR 19 has already given it its own database. Nothing else.
From chapter 10, of the storefront: "What is the peak rate, how long does it last, and on how many days a year does it happen?" Two thousand a second, for two hours, on six days. Ana said plainly that this is the most expensive row and the decision she is least sure of.
From chapter 3, of the same row: "Which part of this decision could we undo in a week, and which part could we never undo?" The units can be switched off in a day. The basket code written for them would have to be rewritten.
From chapter 2, of Marcus's slide: "Which three characteristics decided this design, and which one did you give up to get them?" He answered without hedging. One set of skills, one way to operate, people who can move between teams. Those are the company's needs and belong to no single system, and what the design gives up is fit. He then made the point that settled the meeting: those three are exactly what a platform is for. His standard was adopted for the roads: one platform, one broker, one way to deploy and observe, and one event format between systems. He was right about the system that joins the systems.
Ingrid's question was her own. How many things is the company now running that nobody in it had run two years ago? A broker, a data grid and rented functions: three. She was right about order entry, the nightly batch and the shared database, and her question put a limit on the next year: no fourth.
Then the review recorded one decision in full. It was the oldest one.
ADR 31: Prices are fixed nightly; quotes are worked out on request and stored
Status: Accepted, 14 March. Records a decision first taken twelve years ago and never written down, as amended last May.
Context: Suppliers send price lists once a day. Contracts promise that a morning's price holds all day. Finance and the auditors must be able to reproduce any invoice to the penny for seven years. Since the acquisition, account managers must also be able to change a contract price during the day.
Decision: We will keep the nightly line as the only writer of the price table, and keep each night's table for seven years. During the day a quote is worked out on request by the same pricing component, and the quoted price is stored on the order line. Invoices use the order line's price.
Consequences: Any invoice can be reproduced from its order lines and that night's table. A contract change is in force within five minutes. Harder: a supplier's price change still waits for the night. A price is worked out in two places, the line and the request path, and they must agree to the penny. Nine million rows are recomputed every night whether or not they changed.
Fitness functions: the build fails if anything but the nightly line writes to the price table. Every night one account in a hundred is priced by both paths, and any difference raises an alarm.
Review when: suppliers send prices more than once a day; or more than one order in ten carries a same-day price change; or the nightly line finishes after 05:30 three times in a quarter.
From chapter 1: "Is the original reason for this decision still true today, yes or no, and how do you know?" Two of the three are, Ingrid said, and the record now says which. From chapter 14: "What does the record say became harder?" Three things, in its own words. From chapter 16: "Which check fails the build if this rule is broken, and which decision record does its message name?" The price-table check, and it names ADR 31.
Lena took the three-row slide to the board. Ingrid wrote ADR 31 herself. It took her twenty minutes, and it was the first decision record she had ever written.
Remember it as: for each system, three drivers, one style, the loss you accepted, the check that keeps it true, and the evidence that would reopen it.
What Ana learned
- Review a company as a set of systems, each with its own three drivers, its style and its stated loss.
- Standardise what the systems stand on, and count every new technology as money spent.
- Give every recorded decision a check and a trigger, and when a trigger fires, decide again without embarrassment.
…but
There is no next crack, only the next trigger. In June the suppliers' trade body announced that price lists will be issued twice a day from next year. ADR 31 names that. Ingrid has put the review in the calendar. The epilogue is the plan of the town, for when your own triggers fire.
EpilogueThe Town Plan#
The reference: everything here appeared in the story.
The metaphor table
| Concept | Metaphor | Chapter |
|---|---|---|
| Architecture | The load-bearing walls: what is expensive to move once people live there | 1 |
| Design | The furniture: moved on a Saturday | 1 |
| Accidental architecture | A house extended by every owner, with no drawings | 1 |
| Quality attribute / architecture characteristic | What the building must survive: fire, crowds, floods, burglars | 2 |
| Driving characteristics | The three that decide the building; nobody can afford all of them | 2 |
| Quality-attribute scenario | "When the fire alarm sounds with 300 people inside, everyone is out in four minutes" | 2 |
| Trade-off | The cathedral ceiling and the heating bill | 3 |
| One-way / two-way door | Pouring the foundations / hanging a door | 3 |
| Last responsible moment | Ordering the windows when the walls are up, not before and not after the rain | 3 |
| Cohesion / coupling | Rooms that belong in one building / buildings that share a wall, a boiler or a drain | 4 |
| Connascence | Two things that must change together: the lock and every copy of the key | 4 |
| Component | A wing with its own entrance | 4 |
| Architecture quantum | What must be built, opened and closed together | 4 |
| Layered style | The terrace: every house has the same floors in the same order | 5 |
| Sinkhole | A floor you only walk through to reach the stairs | 5 |
| Modular monolith | One building with fire walls between the wings | 5 |
| Pipeline style | The assembly line: each station does one thing and passes it on | 6 |
| Microkernel style | The market hall and its stalls: the hall provides the roof and the rules, the stalls come and go | 7 |
| Service-based style | A campus of a few large buildings sharing one records office | 8 |
| Service-oriented architecture | A campus where every errand goes through the central switchboard | 8 |
| Microservices style | A street of small shops, each with its own till and stockroom | 12 |
| Event-driven style | The town noticeboard: post a notice and whoever cares acts on it | 9 |
| Broker / mediator topology | Notices passed hand to hand / a clerk who reads each notice and assigns the errands | 9 |
| Space-based style | Market stalls that each carry their own stock, and settle with the warehouse afterwards | 10 |
| Serverless | Renting the hall by the hour: no rent when it is empty, and a wait while the lights come on | 11 |
| Granularity | How small a shop can be before the shopkeeper spends the day walking between shops | 12 |
| Choosing a style | Zoning: a factory, a school and a row of houses do not share one plan | 13 |
| Architecture decision record | The planning file: what was decided, when, what was known, and what it cost | 14 |
| C4 model | Map scales: the region, the town, the building, the room | 15 |
| Fitness function | The building inspection, repeated on every change | 16 |
| Architectural drift | The extension nobody applied for | 16 |
| Technical debt | Deferred maintenance, with interest | 16 |
| Evolutionary architecture | Leaving the gable wall plain because a wing will be added there | 16 |
| The architect | The town planner, who walks the streets and also sits in the council meeting | 17 |
| Advice process | Anyone may build, after asking the neighbours and the planner, and filing the plan | 17 |
The town fails in one place, on purpose: a building cannot be rebuilt every week and software can, which is why chapter 16 leaves the metaphor behind.
The review checklist
Every trap in the book, with its tell-tale sign, for use in a review.
Foundations: act I (chapters 1–4)
- Accidental architecture (ch. 1, The Decision Nobody Remembered Making): a constraint everyone works around and nobody can date or explain, described as "it has always done that".
- Architecture as a phase (ch. 1, The Decision Nobody Remembered Making): an architecture document dated before the first commit and not edited since.
- The framework mistaken for the architecture (ch. 1, The Decision Nobody Remembered Making): the sentence "our architecture is Spring Boot, PostgreSQL and Kafka".
- Every characteristic a priority (ch. 2, What Must This Building Survive?): a list of ten or more qualities all marked high, or the words "these are all non-negotiable".
- The unmeasurable characteristic (ch. 2, What Must This Building Survive?): an adjective with no number: "highly available", "fast", "scalable".
- The forgotten implicit characteristic (ch. 2, What Must This Building Survive?): a quality so obvious to the people who run the system that it is on no list, such as "finished before the warehouse opens".
- The generic architecture (ch. 2, What Must This Building Survive?): a design justified as "scalable, secure and resilient" that would suit any company equally.
- Best practice without context (ch. 3, Nothing Is Free): a justification that ends at "it's best practice" with no scenario attached.
- Analysis paralysis (ch. 3, Nothing Is Free): a third workshop about a choice that could be reversed in a week.
- The one-way door walked through casually (ch. 3, Nothing Is Free): "we can always change it later" said of a data format, a published interface or a record being retired.
- The two-way door decided by committee (ch. 3, Nothing Is Free): a steering group scheduled to choose a library or name a queue.
- Benefits listed without costs (ch. 3, Nothing Is Free): a proposal with a gains column only, and silence when someone asks what it makes harder.
- Copying a famous company (ch. 3, Nothing Is Free): a justification that names a firm a thousand times your size.
- The shared library as hidden coupling (ch. 4, What Belongs Together): a
commonorutilslibrary that holds a business rule and is compiled into several programs. - The entity trap (ch. 4, What Belongs Together): components named after tables, such as Customer Manager, Order Manager and Product Manager, each offering create, read, update and delete.
- Dynamic connascence across a network (ch. 4, What Belongs Together): two services that only work when called in a certain order or within a certain time, with a wiki page as the only record.
- Technical partitioning for domain change (ch. 4, What Belongs Together): every business change touches every layer and needs the screen team, the services team and the database team in one meeting.
One building: act II (chapters 5–7)
- The sinkhole (ch. 5, The Terrace): a chain of methods with the same name in each layer, each calling the next and adding nothing.
- The accidentally open layer (ch. 5, The Terrace): a query in a screen or a report that reads tables directly, with no record of anyone deciding it.
- Lasagne (ch. 5, The Terrace): seven or eight thin layers, with facades, managers and mappers between the usual three, so that adding one field edits a file in each.
- Layered by default (ch. 5, The Terrace): nobody can say why the system is layered except that the project template was.
- Filters that share state (ch. 6, The Assembly Line): a static field, a shared working table or a running total that two stations both touch.
- The filter that does everything (ch. 6, The Assembly Line): one station called "process" with six jobs and a thousand lines.
- The pipeline with no re-run story (ch. 6, The Assembly Line): nobody can say, for a failure at station seven at 03:10, which stations run again and what would be written twice.
- Plug-ins that depend on each other (ch. 7, The Hall and Its Stalls): a plug-in that imports another, or only works if another is loaded first.
- The fat core (ch. 7, The Hall and Its Stalls): a plug-in's name in the core, as in
if (courier.id().equals("northline")). - The unversioned contract (ch. 7, The Hall and Its Stalls): a contract changed in place with every plug-in rebuilt the same night.
- A plug-in with access to the core's database (ch. 7, The Hall and Its Stalls): a connection or a repository handed to a plug-in "for convenience".
Many buildings: act III (chapters 8–12)
- The shared schema that ripples (ch. 8, A Handful of Big Buildings): a column rename that needs four services deployed on the same night, or a table nobody can name the owner of.
- The service bus returns (ch. 8, A Handful of Big Buildings): a "gateway" or "integration layer" between the screens and the services that has begun to make decisions of its own.
- Service creep (ch. 8, A Handful of Big Buildings): a service count that goes from five to twenty in a year, each split sensible on its own, all still on one database.
- Event-driven for a question that needs an answer (ch. 9, The Noticeboard): a request event, a reply event and a caller waiting between them with a time limit.
- No error path (ch. 9, The Noticeboard): nobody can say where an event goes after its last failed attempt.
- The event with no owner (ch. 9, The Noticeboard): an event that two services both post, or one whose fields cannot be changed because nobody knows who reads them.
- The flow nobody can see (ch. 9, The Noticeboard): answering "what happens after an order is placed?" means searching every repository for subscribers.
- Space-based for data that must agree (ch. 10, Stalls That Carry Their Own Stock): two units selling the same last item, or approving two orders against one credit limit, and a nightly job that "repairs collisions".
- The grid as system of record (ch. 10, Stalls That Carry Their Own Stock): a data writer that is absent, or has been failing unnoticed.
- Space-based for a steady load (ch. 10, Stalls That Carry Their Own Stock): a unit count that never changes.
- Function pinball (ch. 11, Renting by the Hour): one action that bounces through eight functions and five queues, each trivial.
- Serverless for steady heavy load (ch. 11, Renting by the Hour): a function that runs at a constant rate all day.
- The cold start on the critical path (ch. 11, Renting by the Hour): an occasional response of several seconds on something a customer is waiting for, most often first thing in the morning.
- Lock-in ignored, and lock-in feared (ch. 11, Renting by the Hour): either that nobody has estimated what leaving would cost, or that a home-made layer hides every provider service so that none is used well.
- Grains of sand (ch. 12, How Small Is Small Enough?): a single request that crosses five services, and a team of four that owns twenty.
- Splitting for its own sake (ch. 12, How Small Is Small Enough?): a split justified by "smaller is better" or "single responsibility", with no disintegrator named.
- Ignoring the integrators (ch. 12, How Small Is Small Enough?): two services that are always deployed together, or a transaction across two services described as "reliable enough with retries".
- The saga chosen by accident (ch. 12, How Small Is Small Enough?): nobody can say whether a flow across services is meant to be all-or-nothing or to settle later, or who notices a failed step.
Choosing and keeping: act IV (chapters 13–18)
- One style for the whole company (ch. 13, Not One Plan for the Whole Town): a target architecture with one diagram and the word "all".
- The golden hammer (ch. 13, Not One Plan for the Whole Town): the style of the architect's last success proposed for a new system before its drivers have been named.
- Distributed without a reason (ch. 13, Not One Plan for the Whole Town): several deployables and nobody able to say which part must scale, deploy or fail separately.
- The style the team cannot run (ch. 13, Not One Plan for the Whole Town): a design that needs a broker, a grid or a cloud service that nobody on the team has operated, with no line in the plan for learning it.
- Covering your assets (ch. 14, Write It Down): a decision postponed again "until we know more", or a record that lists options and chooses none.
- Groundhog Day (ch. 14, Write It Down): the same decision reopened in three meetings because nobody can find why it was made.
- Email-driven architecture (ch. 14, Write It Down): a decision that exists only in an email thread or a chat channel.
- The record written afterwards and never superseded (ch. 14, Write It Down): a record dated months after the code, tidier than the decision ever was, or an accepted record that the system now contradicts.
- Boxes and lines with no legend (ch. 15, Draw It So They Can Read It): a diagram on which only the author can say what a box, a colour or a dashed line means.
- Mixed levels of abstraction (ch. 15, Draw It So They Can Read It): a class or a table drawn beside a service, in the same shape.
- One diagram for every audience (ch. 15, Draw It So They Can Read It): a single wall-sized drawing shown to the board and to the engineer on call.
- Diagram rot (ch. 15, Draw It So They Can Read It): a diagram older than the last three releases, in a tool one person has.
- The rule on the wiki (ch. 16, Keep It True): an architectural rule that exists as a sentence in a document and nowhere in the build.
- The failing check everyone ignores (ch. 16, Keep It True): a check that has been red, or set to "warn only", for a month.
- Debt with no register (ch. 16, Keep It True): "we'll fix it later" with no list, no owner and no date.
- The big-bang re-architecture (ch. 16, Keep It True): a plan to stop other work and rebuild everything on the target, delivering nothing until the end.
- The ivory tower (ch. 17, The Architect in the Room): decisions that arrive as documents from people who have not been in a team's code, or on call, this year.
- The architect as bottleneck (ch. 17, The Architect in the Room): a queue for architectural approval measured in weeks.
- The frozen caveman (ch. 17, The Architect in the Room): an architect who raises the same objection to every proposal, from one bad experience years ago, about technology that has since changed.
- Control where guidance would do (ch. 17, The Architect in the Room): an experienced team required to seek permission for choices it could reverse in a day.
- The uniform target architecture (ch. 18, One Town, Three Plans): a target-state diagram that is the same for every system, with a migration plan measured in years.
- A review with no characteristics (ch. 18, One Town, Three Plans): a review that discusses boxes, products and standards and never asks what the system must survive.
- A decision with no trigger for review (ch. 18, One Town, Three Plans): a record that names no evidence that would reopen it.
The style comparison table
Chapter 13's table, unchanged. For cost, Good means cheap.
| Style | Deployability | Elasticity | Fault tolerance | Performance | Simplicity | Testability | Evolvability | Cost |
|---|---|---|---|---|---|---|---|---|
| Layered | Poor: every change ships everything | Poor: scales only as a whole | Poor: one process, one failure | Fair: in-process calls, one database | Good: one program, familiar to everyone | Fair: layers stubbed, changes cross them | Poor: business changes cross every layer | Good: one build, one thing running |
| Modular monolith | Poor: one release for every module | Poor: scales only as a whole | Poor: one process, one failure | Fair: in-process calls, one database | Good: one program, enforced borders | Good: a module tests alone | Good: business change stays in module | Good: one build, one thing running |
| Pipeline | Poor: the whole line ships together | Poor: stations scale together | Fair: restartable only if designed | Fair: sequential, suits batches | Good: one direction, one contract | Good: file in, file out | Good: add or swap a station | Good: cheap to build and run |
| Microkernel | Fair: stalls can ship without core | Poor: everything under one roof | Poor: a bad plug-in hurts all | Fair: in-process calls through contract | Good: small core, uniform plug-ins | Good: plug-in tested against contract | Good: add a plug-in, touch nothing | Good: one deployable to run |
| Service-based | Good: each team ships alone | Fair: coarse services, shared database | Fair: services isolated, database shared | Fair: few network hops | Fair: simplest of the distributed styles | Good: one business area per service | Good: cut along business lines | Fair: few services, data unmoved |
| Event-driven | Good: processors ship alone | Good: scale each by queue length | Good: notices wait for slow readers | Good: nobody waits for anybody | Poor: the flow is written nowhere | Poor: behaviour depends on timing | Good: new reader, poster unchanged | Fair: a broker and its upkeep |
| Space-based | Fair: many identical units, one grid | Good: add units, no shared queue | Good: every unit holds a copy | Good: answers come from memory | Poor: replication, pumps and collisions | Poor: needs load to reproduce | Fair: code is built around grid | Poor: data in memory many times |
| Serverless | Good: each function ships alone | Good: zero to thousands, unattended | Good: provider restarts and retries | Poor: cold starts, limits per run | Fair: trivial pieces, intricate wiring | Poor: provider's services not on laptops | Fair: tied to one provider's services | Good: nothing while idle; watch crossover |
| Microservices | Good: every service ships alone | Good: scale one capability alone | Good: failures stay in one service | Fair: every interaction is a hop | Poor: sagas, contracts, many moving parts | Fair: parts easy, the whole hard | Good: small parts replaced independently | Poor: a build and pager each |
Two templates
A quality-attribute scenario (chapter 2):
Scenario: <the characteristic, as an event someone could watch>
Source:
<Who or what causes it>
Stimulus:
<What they do, or what happens>
Environment:
<The conditions at the time: normal load, peak, during the nightly run>
Response:
<What the system does>
Measure:
<The number and the frequency that say it passed>
A decision record (chapter 14):
ADR <number>: <the decision, as a short phrase>
Status:
<Proposed, Accepted or Superseded, with the date and the record it supersedes or is superseded by>
Context:
<What was true and what mattered on the day: drivers, constraints, numbers>
Decision:
<One or two sentences beginning "We will">
Consequences:
<What becomes easier, and at least one thing that becomes harder>
Review when:
<The evidence that would reopen this decision>
Once the rule has a check, add a line Fitness functions: naming it, as ADR 31 does in chapter 18.
The diagram legend
The four notation rules of chapter 15: state the level under every structure diagram; label every arrow with a verb, and between containers with a protocol or medium; keep one level of abstraction on a diagram; and give a legend. This book's legend is the colours above, plus three conventions: a frame around several boxes is one deployable, a solid arrow is a call that waits, and a dashed arrow is asynchronous.
The timeline
To 1999: the vocabulary. Structure came first: layers, modules, pipes, coupling and cohesion. In the 1990s architecture became a subject with a definition, a catalogue of styles and the claim that qualities drive structure.
2000 to 2014: the practice. Reviews gained scenarios and trade-off points, decisions gained a deferral rule and then a one-page record, diagrams gained scales, and the service bus rose and fell.
2015 onward: the judgement. The styles were catalogued and rated, granularity got forces in place of sizes, and governance moved from review boards to checks in the build and advice between teams.
The reading list, by year
These are the milestones above. The sources behind each Still argued block are in research/milestones.md, with the date and the claim each was checked for.
- 1964 Doug McIlroy, Bell Labs memorandum on connecting programs.
- 1968 Edsger Dijkstra, "The Structure of the 'THE'-Multiprogramming System".
- 1972 David Parnas, "On the Criteria To Be Used in Decomposing Systems into Modules".
- 1973 Carl Hewitt, Peter Bishop and Richard Steiger, "A Universal Modular ACTOR Formalism for Artificial Intelligence".
- 1974 Wayne Stevens, Glenford Myers and Larry Constantine, "Structured Design".
- 1985 David Gelernter, "Generative Communication in Linda".
- 1992 Dewayne Perry and Alexander Wolf, "Foundations for the Study of Software Architecture". Meilir Page-Jones, "Comparing Techniques by Means of Encapsulation and Connascence". Ward Cunningham, "The WyCash Portfolio Management System".
- 1993 David Garlan and Mary Shaw, "An Introduction to Software Architecture".
- 1995 Philippe Kruchten, "The 4+1 View Model of Architecture".
- 1996 Buschmann, Meunier, Rohnert, Sommerlad and Stal, Pattern-Oriented Software Architecture, Volume 1.
- 1998 Len Bass, Paul Clements and Rick Kazman, Software Architecture in Practice. Read the scenario chapter in a later edition.
- 2000 Rick Kazman, Mark Klein and Paul Clements, ATAM: Method for Architecture Evaluation. Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures.
- 2003 Martin Fowler, "Who Needs an Architect?". Mary and Tom Poppendieck, Lean Software Development. Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns.
- 2005 Thomas Erl, Service-Oriented Architecture: Concepts, Technology, and Design.
- 2010 ThoughtWorks, Technology Radar, first edition.
- 2011 ISO/IEC 25010. Michael Nygard, "Documenting Architecture Decisions". Simon Brown, the C4 model (c4model.com).
- 2014 AWS Lambda, announced in November.
- 2015 Mark Richards, Software Architecture Patterns. Neal Ford and Mark Richards, Service-Based Architectures (video). Dan McKinley, "Choose Boring Technology".
- 2016 Jeff Bezos, letter to shareholders for 2015. Mike Roberts, "Serverless Architectures" (revised 2018).
- 2017 Neal Ford, Rebecca Parsons and Patrick Kua, Building Evolutionary Architectures.
- 2020 Mark Richards and Neal Ford, Fundamentals of Software Architecture. If you read one more book, read this one. Gregor Hohpe, The Software Architect Elevator.
- 2021 Neal Ford, Mark Richards, Pramod Sadalage and Zhamak Dehghani, Software Architecture: The Hard Parts. Andrew Harmel-Law, "Scaling the Practice of Architecture, Conversationally".
- 2024 Vlad Khononov, Balancing Coupling in Software Design.
For where to draw the borders inside a system, read DDD and Microservices: The Atlas. For data, scale and staying up, read System Design: The Depot.