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 phút đọc
Trang này chưa có bản Tiếng Việt, nên bạn đang đọc bản gốc tiếng Anh.
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.
| Term | Plain meaning | Example |
|---|---|---|
| Module | A part of the code with one job, and everything that job needs | An orders folder with its API, logic and data access |
| Boundary | The edge of a module; other code may only go through its front door | Other modules call orders.placeOrder(), never its tables |
| Interface or contract | The front door: what a module promises to others | A function signature, an HTTP API, an event’s shape |
| Coupling | How much one part must know about another to work | Payments breaks whenever orders renames a column |
| Cohesion | How closely the things inside one module belong together | Everything about refunds lives in one place |
| Monolith | One deployable application | One repository, one build, one deploy |
| Service | A separately deployable application with its own data | A 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.
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 coupling | Weaker coupling |
|---|---|
| Reading another module’s database tables | Calling its API |
| Sharing internal classes or types across teams | Sharing 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 code | Each 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.
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.
Monolith, Modular Monolith or Microservices?
These four decisions add up to an architecture style. The common options, from simplest to most distributed:
| Style | Good at | Costs | Fits when |
|---|---|---|---|
| Monolith without boundaries | Starting fast | Every change risks breaking something unrelated | Prototypes you plan to throw away |
| Modular monolith | Simple to run, test and debug; boundaries can still be enforced (lint rules, module APIs) | One deploy for everyone; one scaling profile | Most products and teams, most of the time |
| Microservices | Independent deploys and scaling per service | Network failures, versioning, monitoring and data consistency across services | Several teams that need to ship independently, around boundaries that have proven stable |
| Distributed monolith | Nothing | Costs of both: services that must still be released together | Never 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 1: one app and one database, but the code is already split into
orders,paymentsandcatalog, 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
paymentsis 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-way | Usually one-way |
|---|---|
| A library inside one module | The database and data model behind a core entity |
| Internal function signatures | Public API and event contracts |
| A UI framework for a new, isolated app | Splitting or merging services |
| Build tooling | Who owns which data |
| Code style | Identity 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:
# 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
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/importspayments/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:
- Expand: add the new column or table, nullable, next to the old one
- Write both: deploy code that writes old and new
- Backfill: copy existing data into the new shape
- Read new: switch reads to the new shape, behind a flag
- 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.
Tình huống 1
Microservices From Day One
A new startup with six engineers wants to build its product as microservices “so it can scale later”.
Xem đáp ánẨn đáp án
- 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.
Tình huống 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.
Xem đáp ánẨn đáp án
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.
Tình huống 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.
Xem đáp ánẨn đáp án
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.
Tình huống 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.
Xem đáp ánẨn đáp án
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:
- Move logic that changes together into one service (merge services or move a capability).
- Make contracts backward compatible so releases no longer need an order (versioned APIs, expand and contract).
- 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
- Martin Fowler: Conway’s law and Parallel change
- Matthew Skelton and Manuel Pais: Team Topologies, key concepts
- Thoughtworks Technology Radar: Architectural fitness function
- dependency-cruiser and ArchUnit
- Nicole Forsgren, Jez Humble and Gene Kim: Accelerate (IT Revolution, 2018), and DORA: Loosely coupled teams
- David L. Parnas: On the criteria to be used in decomposing systems into modules (1972)
- Michael Nygard: Documenting architecture decisions
- Jeff Bezos, 2015 letter to Amazon shareholders, on Type 1 and Type 2 decisions, summarized with quotes by Farnam Street: Reversible and irreversible decisions
- Sam Newman: Monolith to Microservices (O’Reilly, 2019)
- InfoQ: Segment’s move from microservices back to a monolith (2018), summarizing Alexandra Noonan’s “Goodbye microservices”
- InfoQ: Prime Video’s monitoring service moving from serverless to EC2 and ECS (2023)
- Martin Fowler: Monolith first
Thẻ
Câu hỏi thường gặp
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.