Architecture & platform

Articles

On this page
  1. A river needs a bed
  2. Play, build, rest, refactor
  3. Let software emerge
  4. Domain-Driven Design
  5. Capabilities and contracts
  6. Transparency over perfection
  7. Early feedback, no fear
  8. Who builds it
  9. Structure that makes room
  10. Books that shaped this

Let software emerge: how I think about architecture

± 16 min read Published

Executive summary

  • Idea. Structure and flow need each other. A boundary is not there to restrict, it is there to make room inside it.
  • How. Four steps: play, build, rest, refactor. The last one, paying off the debt taken on while playing and building, is the one that usually gets skipped, so here it is a fixed step.
  • Emerge. Do not abstract too fast. Duplicate a few times inside a context, then see what is really generic and deduplicate.
  • Architecture. Domain-Driven Design: each capability in its own boundary, dependencies that run one way and are guarded by a tool, and the decisions behind that written down as short records.
  • Contracts. Capabilities talk through contracts owned by the one that asks, each with a null answer, so a capability can be switched on or off without breaking its neighbours.
  • Transparency. Perfection is an over-constraint. Name the shortcut, put it on a list and pay it off as a step of its own.
  • Feedback. Show things early, and make releasing calm enough to do it often, so feedback can come in often.
  • Who builds it. The same shape around the builders: named roles, explicit mandates and defined handovers, for people and just as much for an organisation of AI agents.

This is how I think about building software: the way of working and the architecture behind it. The examples below are fictional, from a small webshop. How I applied all of this to a real app is told in the Pack a Thing article.

A river needs a bed

Structure and flow need each other. Too much structure and the flow stagnates; too little and there is no direction, so the flow stops as well. A river needs a bed, and a bed without a river is a ditch.

The same water three times: without banks it fans out, between banks it meanders and travels on, and behind a dam it fills up into a lake and stands still No banks it spreads out and stops A bed it meanders, and it gets somewhere A dam it fills up into a lake and stands still
Structure and flow need each other. Without banks the water fans out until nothing moves; between them it still finds its own way, and it travels; dam it completely and it turns into a lake, with a dry bed below. The bed is not there to hold the river back, it is what lets it go somewhere.

That is a statement about software as much as about a day. A boundary in a codebase is not there to restrict anyone, it is there to create room inside it: if the edges of a part are clear, you can move freely within them without having to hold the whole system in your head. Most of the decisions further down come from that one idea.

The same shop as one tangle of parts that all know each other, and as three capabilities with a boundary each and dependencies running one way One big ball of mud Orders Mail Stock Prices Cart Admin PDF Users every change can reach everything Boundaries you can read Ordering Cart Checkout Stock Items Levels Shipping Parcels Labels one way one way a change stays inside its boundary
The parts on the left are not worse written, they are simply all reachable from everywhere, so every change has to be thought through against the whole. On the right the same parts sit inside a boundary, and the dependencies run one way, so a change has an edge to stop at.

I write about that balance between structure and freedom in its own right, in Dutch, on czyk.nl/flow.

Play, build, rest, refactor

In software development we build straight for production far too often. The thing ships, it is legacy the same week, debt has been added, and we move on to the next thing immediately. That is how the debt grows.

My way of working is different, in four steps.

Play first. Allow yourself to play before anything has to be good or finished. It can even be fun and joyful, because there is no pressure yet to get it done and to do it right. That holds for software, and just as much for a hands-on project where you have a new discipline to learn. During playtime it is far easier to throw something away and start over, because nothing depends on it yet.

Then build. Reflect on the play, make a plan, build it and take it to production. The knowledge and the information are already there; you only have to carry it out. That is what makes this step easy instead of heroic.

It does not have to be production, as long as it is genuinely in use. A test environment counts, provided people actually work in it, and so does a feature toggle that opens the thing to one group first. Use is the point: without it nothing comes back, and the next step has nothing to look at.

Let it rest. Gather data, watch how it is actually used, write the insights down, and keep an eye on the error logs as they come in, so a bug gets fixed before anyone has to report it. Not a pause — the step that tells you what the next one should be.

Rest matters for another reason too: it puts you in a different mindset. While you build you are on your way to a goal, fixing problems, fixated on the target. When you let it rest, room appears and your view widens. From that place you can reflect better and see patterns, or see them again.

Then refactor. Rewrite or adjust the thing that is already in production, so the shortcuts and the debt you took on get resolved. This is the step that gets skipped most of the time, and it is the important one. It is how you end up with stable, reliable, reusable parts: debt goes down, modularity goes up, and later you can build on what is there instead of having to refactor it first, with all the risk that carries as complexity grows.

Let software emerge

You can design software up front, but that is often harder than it looks, because you do not know reality well enough yet. You do not have the insight or the feedback. I found it much easier to let software emerge: try things, put them live, and then look at how they are used, look at the data, look at where duplication appears.

That means allowing yourself not to abstract too fast. Duplicate, inside the context you are working in. After a few duplications it is fairly easy to see what is really generic, what is worth abstracting, and what is still specific and should stay inside its bounded context. It touches the principle of “don’t repeat yourself”, but I always say: do repeat yourself. At least a few times.

From there you can lean back and look at which piece you actually want to reduce. Those are always nice changes, because you built the work without having to think about the perfect generic model. You get speed, you may duplicate a little, and afterwards you deduplicate into the piece that is generic and highly reusable.

Domain-Driven Design keeps it readable

The architecture follows Domain-Driven Design, so that the concepts and the processes in the code are readable and understandable by a human being. An order is an order, a stock item is a stock item, and ordering asks stock for something rather than reaching into it.

The seam between two parts is the clearest example. In a webshop, ordering holds what a customer wants, and shipping is nothing but parcels on a day. So ordering does not reach across and write a delivery date, it asks for the order to be shipped. Shipping does not know about the checkout: the order asks, shipping answers. If the shop switches shipping off, say it only offers pickup, the order simply stays a plain order, without a date. Nothing breaks, because the one that asks never depended on the answer. That is not an implementation detail, it is a decision about who knows what.

Each capability lives in its own directory with its own README and its own descriptor, and carries a subdomain type — core, supporting or generic — so it is explicit which parts deserve the care and which parts merely have to work. Dependencies between capabilities are guarded by a tool rather than remembered by anyone's memory, and the decisions that set those rules are written down as short architecture decision records: directories follow boundaries before types, the asker owns the contract, the centre discovers, never enumerates.

Circular dependencies get a rule of their own, because two capabilities that depend on each other are not two capabilities: they are one system wearing two names. An ADR forbids them, and an application architect stands guard over that line. That does not make a violation a disaster. While playing and while building they are allowed to appear, because stopping to be correct is exactly what kills play. They come out afterwards, in the refactoring step, through a process that goes over the architecture and refactors it back into line with the modular design. The same four steps, applied to the shape of the system itself.

The same boundaries can be visible from outside the code, too. If a product offers an API or tools for assistants, group them the way the capabilities are grouped: one surface per capability, so what is separate inside stays separate outside.

Most of the philosophy behind that architecture comes from The Anatomy of Domain-Driven Design by Scott Millett and Samuel Knight, which in turn builds on Eric Evans' Domain-Driven Design. Clean Code, The Clean Coder and Clean Architecture by Robert C. Martin shaped it as well, and so did Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. The refactor step comes from Refactoring by Martin Fowler.

Capabilities that switch on and off

A capability can be turned on or off without anything around it breaking. Which capability a customer is offered can follow from a plan, a region or the stage a feature is in: in a webshop, think of gift wrapping or loyalty points. That works because of how the capabilities talk to each other, and it is the opposite of a big ball of mud.

Capabilities do not reach into each other. When one needs something from another, it declares a contract: the capability that asks the question owns the interface. The checkout wants to know whether an item is in stock, so Checkout owns that question and Stock answers it. The arrow therefore points from the answerer to the asker, which feels backwards until you see what it buys: the asker never names its answerer, and ships a null answer of its own. If nobody answers, Checkout still renders and still runs, and simply shows less. There is no check for whether something exists, and no flag woven through the code.

Functionality is enriched by adding an answerer, not by editing the thing that asks. Reporting collects numbers from the capabilities in exactly the same way: it owns the question, and each capability answers what only it can count. A new capability joins by implementing contracts that already exist, and switching one off leaves the others standing.

It has a price, and I would rather name it. Arrows often point against intuition and against the direction the data flows, so a diagram alone cannot tell this pattern from a real breach. And every contract costs an interface, a null answer and their tests. That is only worth it where a boundary is real. Where it turns out not to be, the answer is to merge the two and drop the machinery.

A good test of the idea is a nightly job that deletes what has expired, in a shop the abandoned baskets and the old delivery addresses. The job does not know what is kept. Each capability registers its own retention policy, saying how long it keeps its rows and what “gone” means for them, and the job only walks the list. The centre decides when, the owner decides what, and a new capability joins by registering a policy instead of being added to the job.

The same goes for the rules that matter most. If a rule only holds because every developer remembers it, it will be broken sooner or later. In a webshop, keep customer addresses in a store of their own, reachable only through one front door that logs and audits every read. Then logging is not a convention, it is a property of the only way in: there is no route around it, not even from the shop’s own code. Make the right thing the only possible thing, and the rule no longer depends on anyone’s memory.

A contract between two capabilities, with the answering capability switched on and off Stock switched on Checkout the asker contract is this item in stock? Stock answers the answerer depends on the asker, never the other way round Stock switched off Checkout still renders, still runs contract answered by the null version Stock off nothing breaks; the page simply shows less
The capability that asks the question owns the contract and has a null answer of its own, so it works when nobody answers. Switching a capability on or off does not touch the ones around it.

That is what makes the capabilities independent: each one can be used and taken on its own, and together they still make one product.

Transparency over perfection

The same philosophy has a consequence that is easy to get wrong: do not strive for perfection, because perfection is an over-constraint. Say a webshop wants to promise in its terms that “100% of the data stays inside the EU”. That makes it very hard for itself, and the shop would stagnate on every vendor that does not qualify. Or, worse, the sentence would quietly stop being true.

So say what is true, and be transparent about it, in the terms and in the product. Transparency is more important than perfection, which is stagnation, or than misinformation, which is worse.

It is not a licence, though, and the limit is worth naming. Some shortcuts can be paid off completely later: a manual step, a hard-coded list, a screen that does not exist yet. Others cannot. Data that has been handed to a vendor stays handed over: swapping that vendor for another later does not take it back, and it can still leak from where it already is. For a shortcut like that, transparency is not enough on its own. Choose carefully before the first byte goes out, and send as little as you can.

That is also why such a vendor should sit behind a contract to the outside world, modelled the same way as the contracts between capabilities. The shop asks the question, the vendor is one answerer, and swapping it for another does not mean refactoring any internal code. The seam makes the next swap cheap and keeps what is shared small. It does not undo the past.

Seen through the four steps, it looks like this. A shortcut is something you knowingly take on in step two and three, while playing and building, and name explicitly. By making it explicit and visible, step four, refactoring, gets done as well. If it stays implicit it gets forgotten and lost, until it shows up as a data leak and a big oops. This way the debt is on a list, with a name next to it, and it is paid back on purpose.

Early feedback and releasing without fear

Two more things belong to the same philosophy. The first is early feedback. An idea is cheapest to change while it is still early, so I would rather show it to the people it is for before it is finished than polish it alone. A vote or a remark on something half-built tells you more than your own guess about it.

The second is releasing without fear. Fear of a release is a sign that the way to production is not trustworthy, and fear makes people release rarely, in big steps, which is exactly what makes a release dangerous. So the way there gets the same care as the code: small changes, a check at every stop, changes that only add before they take away, and a release that can be undone. When going live is a calm, boring act, you can do it often, and feedback can come in often.

The same shape around who builds it

The idea does not stop at the code. In any organisation that builds software, roles and teams are named, each role has a mandate and, just as importantly, explicit restrictions, and the moments where work moves from one role to the next are defined handovers rather than a shrug. The structure around the builders is a river bed too: without it the work does not go faster, it goes everywhere, because somebody without a boundary will cheerfully redesign something nobody asked about.

A simplified org chart: product owner and enterprise architect set policy, the scrum master guards the process, engineers build and support feeds back refine refine sized, then handed over what users hit Product Owner what matters next Enterprise Architect the rules, and guarding them Scrum Master sizes it, guards the handovers Support hears it first, feeds it back Engineers build it and take it live sets policy guards the process builds and runs
Nothing is picked up that has not been refined and sized first, and each of those steps is a handover to a different role rather than the same one carrying on. The roles that set the rules are not the ones that build, and what users run into comes back to the top.

That is a statement about the real world, about people and teams. It also carries over. If you set up an organisation of AI agents, it makes sense to model the same roles and mandates you would use for people, for the same reasons. The agents do not need a different kind of structure, they need the one that already works. How that looks in practice, with an org chart of AI roles, is part of the Pack a Thing article.

Structure that makes room

None of this is meant to be impressive on a diagram. The boundaries are there so that the next change is easy, the contracts so that a part can be switched off without a fuss, and the four steps so that the debt gets paid instead of inherited. A river needs a bed, and then it is the river you came for.

The product that all this was applied to is told in the Pack a Thing article.

Books that shaped this

Not sources I looked things up in for this article. These are the books that inspired the way I came to think about it, over the years. Each one links to its product page on czyk.nl.