Naar de inhoud
Clinford Jordano Valencia
Alle artikelen

Architectuur & Systemen

The Architecture Decisions That Matter More Than Your Tech Stack

Coupling, boundaries, data ownership and independent deployment decide how fast a system can change. How to recognize those decisions and make them deliberately.

  • 13 min lezen

Deze pagina is nog niet beschikbaar in het Nederlands, dus je leest het Engelse origineel.

Teams spend weeks choosing between React and Vue, or Go and Kotlin, and then lose years to decisions they made in an afternoon: one shared database for everything, services that must be deployed together, a “utils” package that every module imports. The stack sets your starting point. Boundaries, coupling, data ownership and deployability decide how fast you can change later. This post explains those decisions, how to spot the expensive ones, and how to record them.

Why It Matters

The research behind Accelerate (the DORA program) found that teams with loosely coupled architectures, where teams can test and deploy without coordinating with others, deliver more often and with fewer failures, regardless of whether they ran on mainframes or the cloud. In other words, what predicted performance was the shape of the system, not the technology in it.

The Vocabulary in Two Minutes

If you are newer to architecture, these are the words the rest of this post uses. Software architecture is simply the set of decisions that are hard to change later: like the walls of a building, as opposed to the furniture.

TermPlain meaningExample
ModuleA part of the code with one job, and everything that job needsAn orders folder with its API, logic and data access
BoundaryThe edge of a module; other code may only go through its front doorOther modules call orders.placeOrder(), never its tables
Interface or contractThe front door: what a module promises to othersA function signature, an HTTP API, an event’s shape
CouplingHow much one part must know about another to workPayments breaks whenever orders renames a column
CohesionHow closely the things inside one module belong togetherEverything about refunds lives in one place
MonolithOne deployable applicationOne repository, one build, one deploy
ServiceA separately deployable application with its own dataA payments service with its own database

Good architecture means high cohesion inside each module and low coupling between them, so most changes stay inside one module and its team.

The Decisions That Compound

1. Where You Draw Boundaries

A boundary is a line that code and data don’t cross except through a deliberate interface. The classic guidance, from David Parnas in 1972, still holds: split a system around decisions likely to change, so each change stays inside one module. Splitting by technical layer (controllers, services, repositories) spreads every feature across all of them; splitting by business capability (orders, payments, identity) keeps most changes local.

Split by technical layercontrollers/discountservices/discountrepositories/discountone feature touches every layerSplit by business capabilityorderspaymentsusersapilogicdataapilogicdataapilogicdatathe discount change stays inside orders
The same codebase organized two ways. Split by layer, one feature touches every folder; split by capability, it stays in one place.

Layers are still useful inside a module. The point is which line comes first: put the business capability on the outside, so the folder structure matches the way the product changes.

2. How Tightly Parts Are Coupled

Coupling is how much one part must know about another to work. Some coupling is unavoidable; the goal is to keep it weak and explicit:

Stronger couplingWeaker coupling
Reading another module’s database tablesCalling its API
Sharing internal classes or types across teamsSharing a small, versioned contract
Synchronous call chains (A waits for B waits for C)Events or queues where the caller doesn’t need to wait
Deploying together because of shared codeEach part released on its own

Synchronous call chains deserve a closer look, because they couple services in time: each one is only up when everything it calls is up.

Synchronous chain: each call waits for the nextCheckoutPricingInventory99.9% × 99.9% × 99.9% ≈ 99.7%: about 26 hours down a year, not 9With a queue: checkout records the order and moves onCheckout“order placed”Inventory, later
A synchronous chain multiplies failure: three services at 99.9% give a checkout that works about 99.7% of the time. A queue lets the slow part catch up later.

Queues aren’t free: the order is now “placed” before inventory has confirmed it, so the system needs a way to handle “sorry, that item just sold out”. That trade-off (availability now, consistency a little later) is an architecture decision, and it should be made on purpose.

3. Who Owns Which Data

Shared databases are the most common hidden coupling. When two services write the same table, neither can change its schema without the other, and nobody owns the data’s correctness. Give every important piece of data one owner that is allowed to write it, and let everyone else read through that owner’s API or a published copy.

4. Whether Parts Can Be Deployed Independently

If changing the checkout page requires a coordinated release with three other teams, you have a distributed monolith: the costs of distribution (network failures, versioning) without the benefit (independent change). Independent deployability is the test that tells you whether your boundaries are real.

Coordinated releasesOrdersPaymentsShippingOne shared databaseshared-utils v1Independent releasesEvents: “order placed”OrdersPaymentsShippingorders DBpayments DBshipping DBeach service owns its data
Same features, different boundaries. Shared data and code force coordinated releases; owned data and explicit contracts let each part ship alone.

Monolith, Modular Monolith or Microservices?

These four decisions add up to an architecture style. The common options, from simplest to most distributed:

MonolithorderspaymentsusersshippingModular monolithorderspaymentsusersshippingMicroservicesorderspaymentsusersshippingone deploy, tangled codefast start, slows downone deploy, clear modulesthe usual best startseparate deploysfor independent teams
Three common styles. The modular monolith keeps the clear boundaries of services without the cost of running many deployments.
StyleGood atCostsFits when
Monolith without boundariesStarting fastEvery change risks breaking something unrelatedPrototypes you plan to throw away
Modular monolithSimple to run, test and debug; boundaries can still be enforced (lint rules, module APIs)One deploy for everyone; one scaling profileMost products and teams, most of the time
MicroservicesIndependent deploys and scaling per serviceNetwork failures, versioning, monitoring and data consistency across servicesSeveral teams that need to ship independently, around boundaries that have proven stable
Distributed monolithNothingCosts of both: services that must still be released togetherNever on purpose; it is what you get when you split before the boundaries are right

A good path for most products: start as a modular monolith, enforce its boundaries, and extract a service only when a specific module needs its own deploys, scaling or team.

Neither story means “microservices are bad”. Both mean the boundaries should follow the needs of the work (its change rate, load and team structure), not what is fashionable.

A Worked Example: An Online Shop Growing Up

An Online Shop Growing Up

Year 1Year 2Year 3orderspaymentscatalogone databaseorderspaymentscatalogtablestablestablesorderscatalogpaymentsserviceapp tablesits DBmodules split by capabilityshare one databaselint blocks internal importseach module owns its tablespayments extractedbehind the same API
The same shop over three years. Clear modules (year 1) and owned data (year 2) are what make extracting payments in year 3 a deployment change, not a rewrite.
  • Year 1: one app and one database, but the code is already split into orders, payments and catalog, each with its own folder and API.
  • Year 2: a lint rule blocks imports into other modules’ internals, and each module owns its tables.
  • Year 3: a separate payments team needs weekly compliance releases, so payments is extracted as a service behind the same API.
  • Later: another module is extracted only when a team, scaling or compliance need appears.

What made year 3 cheap were the decisions in years 1 and 2. Because payments already had one owner, its own tables and a clear API, extracting it was a deployment change rather than a rewrite. Boundaries drawn early are what make later options cheap, whatever the stack.

One-Way and Two-Way Doors

Not every decision deserves a meeting. A useful test, popularized by Amazon, separates two-way doors (easy to reverse: decide fast) from one-way doors (expensive to reverse: decide slowly and write it down):

Usually two-wayUsually one-way
A library inside one moduleThe database and data model behind a core entity
Internal function signaturesPublic API and event contracts
A UI framework for a new, isolated appSplitting or merging services
Build toolingWho owns which data
Code styleIdentity and tenancy model

The trick is to turn one-way doors into two-way doors where you can: hide a vendor behind an interface you own, version public contracts, and keep data access behind one owner so storage can change later.

Write Decisions Down

Architecture knowledge decays when it lives only in people’s heads. Architecture decision records (ADRs) are short documents, one per decision, kept next to the code:

docs/adr/0007-orders-own-their-data.md
# 7. Orders own the order data
 
Status: accepted · 2026-08-12
 
## Context
Payments and shipping both write to the orders table, so schema changes need three teams.
 
## Decision
Only the orders service writes order data. Others read through its API or the
"order placed" and "order updated" events.
 
## Consequences
Payments and shipping change independently. We accept eventual consistency
(up to a few seconds) in the shipping dashboard.

Three sections are enough: context (why now), decision (what), consequences (what we accept). The consequences section is the most valuable part a year later.

Going Deeper: Teams, Guardrails and Changing Data

Architecture Mirrors the Organization

Melvin Conway observed in 1968 that systems end up mirroring the communication structure of the organizations that build them. Two teams that rarely talk produce two systems with a thin, formal interface; one team that owns everything produces a system with no internal boundaries at all.

Conway’s Law

TeamsCheckout teamPayments teamCatalog teammirrorsSystemscheckoutpaymentscatalog
Conway’s law: the boundaries in the system follow the boundaries between teams. Design the teams and the architecture together.

The practical consequence is sometimes called the inverse Conway manoeuvre: design the teams you need for the architecture you want.

  • Organize stream-aligned teams, each owning a business capability end to end, with a cognitive load small enough to genuinely own it.
  • Don’t split one team’s system into ten services. That only adds network calls.
  • Don’t spread one capability across services owned by others. That creates a distributed monolith.

Encode Boundaries as Tests

Boundaries agreed in a meeting erode one convenient import at a time. Senior engineers turn them into fitness functions: automated checks that fail the build when the architecture drifts.

  • Dependency rules: “nothing outside payments/ imports payments/internal”, enforced with tools such as dependency-cruiser (JavaScript and TypeScript) or ArchUnit (Java).
  • Ownership: every directory has a code owner who reviews changes to it.
  • Budgets: a page’s JavaScript size, a service’s p99 latency or the number of synchronous calls on a critical path, checked in CI or by monitoring.

Change Data Without Downtime

Data models are the hardest one-way doors, because data outlives code. The safe way to change a schema shared by running services is expand and contract (also called parallel change), where every step is backward compatible and can be deployed and rolled back on its own:

  1. Expand: add the new column or table, nullable, next to the old one
  2. Write both: deploy code that writes old and new
  3. Backfill: copy existing data into the new shape
  4. Read new: switch reads to the new shape, behind a flag
  5. Contract: stop writing the old shape, then drop it

It takes more deployments than a single migration, but at no point do old and new code disagree about the data, which is what makes the change reversible.

Signs Your Architecture Is Fighting You

  • A small feature needs changes in many services, or many teams in the same release.
  • People are afraid to change a shared library or table.
  • You can’t run or test one part without starting the whole system.
  • Incidents spread: one slow service makes unrelated features fail (more on this in distributed systems fail in the gaps between teams).
  • “Temporary” workarounds outlive the people who wrote them (see technical debt is not the enemy).

Test Yourself: Scenarios

Think through your answer, then compare.

Scenario 1

Microservices From Day One

A new startup with six engineers wants to build its product as microservices “so it can scale later”.

Antwoord tonen
  • The cost: six engineers can’t staff what ten services need: separate deployments, monitoring, versioning, network failures and data consistency across services.
  • The risk: early boundaries are guesses that move as the product is discovered. Moving one inside a codebase is a refactor; between services it is a migration.
  • Instead: start with a modular monolith: one deployable, modules split by business capability, each owning its tables, boundaries enforced by lint rules.
  • Later: extract a service when a specific module needs independent deployment, scaling or a separate team. The option stays open without paying for it now.

Scenario 2

Two Teams, One Table

The orders and fulfilment teams both read and write the orders table. Every schema change needs a meeting and a coordinated release.

Antwoord tonen

Give the data one owner.

  • Orders owns the table and is the only writer.
  • Fulfilment reads through an API or events such as “order placed” and “order paid”, and keeps its own table for fulfilment-specific state.
  • Migrate gradually with expand and contract: introduce the API or events, move fulfilment’s writes to its own table, then remove its direct access.
  • Record it in an ADR, including the trade-off: fulfilment may see changes a few seconds later.

Scenario 3

Picking a Database for the Core Product

Your team is choosing the database for the product’s core data. One engineer wants a new document database they are excited about.

Antwoord tonen

Treat it as a one-way door: data, queries and operational know-how accumulate quickly and are expensive to move.

  • Decide from evidence: access patterns (transactions? complex queries? scale?), the team’s operational experience, managed hosting options and the cost of being wrong.
  • Prototype the two or three riskiest queries on each candidate.
  • Default to boring: unless the new database solves a specific problem the established one can’t, choose the well-understood option.
  • Keep the door ajar: data access behind one module, so a future move stays possible.
  • Write an ADR with the alternatives considered.

Scenario 4

Every Feature Touches Five Services

In your company, a typical feature needs changes in five services owned by three teams, released in a fixed order.

Antwoord tonen

That is a distributed monolith: the boundaries don’t match how the product changes.

  • Diagnose: map which services each of the last ten features touched. Services that always change together probably belong together.
  • Options, cheapest first:
    1. Move logic that changes together into one service (merge services or move a capability).
    2. Make contracts backward compatible so releases no longer need an order (versioned APIs, expand and contract).
    3. Realign team ownership with business capabilities.
  • Measure progress by how many features now ship from a single team’s change.

Key Takeaways

  • Boundaries, coupling, data ownership and deployability shape delivery speed more than the framework or language.
  • Split around business capabilities and decisions likely to change, not technical layers: high cohesion inside a module, low coupling between modules.
  • Start with a modular monolith and extract services when a team, scaling or compliance need appears; Segment and Prime Video both moved back when the split didn’t fit the work.
  • Give every important dataset one owner that writes it; shared databases are hidden coupling.
  • Decide two-way doors fast and one-way doors carefully, and turn one-way doors into two-way doors where you can.
  • Record significant decisions as short ADRs with context, decision and consequences.

Sources

Tags

Veelgestelde vragen

Should we start with microservices?

NoUsually not. A modular monolith with clear internal boundaries gives you most of the benefits with far less operational cost. Split out a service when a boundary has proven stable and a team needs to deploy or scale it independently.

How do I know if a decision is expensive to reverse?

Ask what would have to change to undo it in a year. If the answer involves migrating data, coordinating several teams or changing public contracts, treat it as a one-way door and decide carefully. If it is contained in one module, decide quickly.

What is a modular monolith?

A single deployable application divided into modules with clear boundaries: each module owns its data and exposes a small API, and other modules can’t reach into its internals. It gives most of the design benefits of services without the cost of running many deployments.

How do I keep module boundaries from eroding?

Make them executable: dependency rules checked in CI (dependency-cruiser, ArchUnit), code owners for each module, and a rule that modules share data only through their APIs or events.

What is an ADR, and where should I keep it?

An architecture decision record is a short document capturing one significant decision with its context, the decision and its consequences. Keep ADRs in the repository next to the code, numbered and never edited after acceptance; a later ADR supersedes an earlier one.

What is eventual consistency, and when is it acceptable?

A system is eventually consistent when different parts may see different data for a short time after a change. It is acceptable when the business can tolerate that delay, such as a dashboard updating a few seconds later, and not for invariants such as account balances.

When should a module become a separate service?

When it has a proven, stable boundary and a concrete need: a team that must deploy independently, very different scaling or reliability requirements, or isolation for security or compliance. Not because the codebase is large.

What is Conway’s law?

The observation that systems mirror the communication structure of the organizations that build them. Team boundaries tend to become system boundaries, so design teams and architecture together.