← All posts

How to Design a System That Will Last 10 Years

Why systems die from early architectural decisions, not outdated tech — and how to design for change, not features: module boundaries, domain modeling, data, ADRs, and a decade-long checklist.

Contents

Most software systems do not die because of technology. They die because of architectural decisions made in the first months of development.

When a project starts, it feels like the main goal is to ship the first version faster. Deadlines press, investors wait for a demo, competitors are already in production. The architect draws a "simple" diagram. The team writes code they will "refactor later." Three years later, "later" becomes "never" — and every change costs as much as a mini-project.

But the true cost of a system is not MVP delivery speed. It is the product's ability to adapt to new requirements without constant rewrites, data loss, and months of business downtime.

Why do some systems evolve successfully for decades — bank accounting, retail warehouse management, B2B CRM — while others turn into expensive technical debt within a few years? The answer is almost never "an outdated framework." The answer is how cheap each next change remains.

This article is for architects, CTOs, team leads, and senior developers who are designing a system today and do not want to explain to the business in five years why "adding one button" takes a quarter.

Key takeaways

Systems die from coupling, not from Java vs Go. The more modules depend on each other, the more expensive every change becomes — and the faster the team stops keeping up with the business.

Design for change, not for features. "How do we implement feature X?" matters for a sprint. "What happens if X changes in three years?" matters for a decade.

The domain outlives technology. Customer, order, invoice, shipment — they survive framework, database, and UI changes. Architecture should reflect the problem domain, not "controller / service / repository" layers.

Universality is the most expensive trap. Builders-of-everything, EAV tables, and "configurable fields for everything" win in demos and lose in production.

Data matters more than code. Code can be rewritten in six months. Migrating a decade of order history from a broken schema takes years and puts the business at risk.

Architecture must outlive its creators. People leave faster than technologies change. Clear structure and ADRs are cheaper than "clever" abstractions only one person understands.

A system lives long not because it was built perfectly. It lives long because each next change stays cheap enough.


Introduction

Architecture is not choosing microservices vs monolith on a Miro board. It is the accumulated effect of decisions about boundaries, data, dependencies, and trade-offs — decisions that seem small when you make them.

"Let's put order status in the shared entities table for now." "We'll make one CommonService for everything." "We'll add configurable fields — the client will set it up." Each such decision saves a day at the start and costs a week three years later.

Long-lived systems share one property: the cost of change grows slowly. Bad systems do the opposite: past a certain threshold, every new feature requires touching half the codebase, coordinating five teams, and hoping nothing breaks in prod.

You cannot predict what the system will look like in 10 years. But you can build it so change does not become a catastrophe. That is what the rest of this article is about.


Chapter 1. Why Most Systems Do Not Reach 10 Years

The typical project lifecycle

The lifecycle of most enterprise systems is predictable. Treat it not as theory but as an early warning checklist: which stage you are in now, and what comes next if nothing changes.

Stage 1. Startup euphoria

The first months are golden:

  • Fast development. A team of five knows all the code. Decisions happen over lunch.
  • Simple architecture. One repo, one database, one deploy. The diagram fits on a napkin.
  • Few constraints. "We'll do it properly later," "refactor after MVP," "this is temporary."

Velocity is high, bugs are few, the business is happy. It feels like it will always be this way. That is an illusion: simplicity is a result of small scale, not good architecture.

Stage 2. Growing requirements

The business grows — and so does the system:

  • New features. Reports, dashboards, a mobile app, partner APIs.
  • New user roles. Manager, accountant, warehouse worker, auditor — each with their own permissions and flows.
  • Integrations. CRM, accounting, marketplaces, banks, logistics.
  • Reports. "We need a pivot by region for the last three years grouped by SKU."

Every new capability is added into the existing structure. If module boundaries were never set, new features are simply "glued" onto what already exists. Coupling grows linearly with every release.

Stage 3. The first workarounds appear

Temporary solutions become permanent:

  • An is_legacy_flow flag on the orders table because "the new process is only for one client for now."
  • Copy-pasted discount logic in three modules — "we'll extract it into a shared service later."
  • A seven-table JOIN for one report — "we'll optimize when we have time."

Workarounds spread faster than refactors. A new developer does not know the history and repeats the pattern. A year later, the "temporary" solution is the only way the system works at all.

Stage 4. Architectural crisis

The symptoms are familiar:

  • Any change breaks neighboring parts. You fix VAT calculation — warehouse reports fail.
  • Development speed drops. What used to take a sprint now takes a quarter.
  • Bug count rises. Regressions after every release. QA cannot keep up.
  • The team fears touching code. "We don't know what will break."

At this stage the business is already losing money: competitors ship features faster, and your "simple tweaks" cost as much as projects.

Stage 5. The big rewrite

The classic ending:

  • A decision to rewrite the system from scratch — "on a modern stack, properly."
  • Lost time and money: two or three years of parallel development, a doubled team, data migration.
  • Repeating old mistakes — because the same people under the same pressure again cut corners on boundaries "until the first release."

A rewrite rarely solves the problem forever. It resets the timer — and the cycle starts again if the approach to design does not change.

Euphoria → Growth → Workarounds → Crisis → Rewrite → Euphoria (v2) → ...
         ↑______________________________________________|
                    (if principles don't change)

Chapter 2. The Main Principle of Long-Lived Systems

Design for change, not for features

Most developers design a system for current requirements. The product owner brings a user story — the team implements it. Architecture "accumulates" from answers to specific sprint tasks.

But requirements always change. A law that does not affect your product today may become mandatory in two years. A client will ask for an integration nobody thought about. The market shifts — and the "obvious" business model ages out.

Long-lived systems are designed differently: change must stay cheap. Not "perfect." Not "beautiful." Cheap — in time, risk, and number of modules touched.

The wrong question

How do we implement this feature?

That question is correct for a two-day task. It is dangerous as the only architectural question. It leads to decisions like "fastest to add a field to the existing table" — without understanding that the table already serves four unrelated domains.

The right question

What happens if requirements change in three years?

Concrete questions worth asking in design review:

Question Why
Which parts of the system must change if rule X changes? Coupling assessment
Can we replace module A without touching module B? Independence
Where is the source of truth for this entity? Single source of data
What if a second sales channel / currency / warehouse appears tomorrow? Extensibility without rewrite
How do we roll back this change? Risk management

Good architecture is architecture where a typical business change is localized. You add a new document type — one module changes. You change a discount rule — one service changes. Not half the monolith.


Chapter 3. Fight Coupling

What coupling is

Coupling is the degree to which a change in one part of the system forces changes in other parts.

Low coupling: the Billing module changes invoice format — the Warehouse module is unaffected.

High coupling: changing order status requires edits in five services, three database triggers, two cron jobs, and the frontend — because logic is "smeared" across the entire system.

Coupling is the main enemy of long-lived systems. Not legacy code. Not an "old" framework. Coupling is what makes legacy incurable without a rewrite.

Examples of high coupling

Shared tables for the whole system

One documents table for orders, invoices, acts, requests, and internal memos. The difference is a type field. Three years later it has 200 columns, half are nullable, indexes do not work, and the business analyst cannot explain which fields belong to what.

Global state

A singleton AppContext service through which half the modules read "current user," "current organization," "current settings." Untestable. Concurrent requests break each other.

Universal entities

Entity + Attribute + Value — classic EAV. "We'll describe everything through metadata." Flexible in a demo. In prod — JOIN upon JOIN, no typing, runtime validation, reports that take weeks to write.

Shared services for every case

CommonService, UtilsService, HelperService — a dump of methods used by 40 modules. Any change is a risk for everyone. Nobody knows who calls what.

Consequences

Consequence How it shows up
No local changes "Can't touch module X — half the system depends on it"
Growing technical debt Workarounds multiply because the "proper" refactor is too expensive
Slower team Onboarding takes months. Code review is a lottery
Fragility One bug — cascade of failures
Fear of change The team adds another bypass layer instead of fixing the root

How to reduce coupling

  • Explicit module boundaries with contracts (APIs, events, interfaces).
  • Ban "convenient" access to other modules' tables and internal classes.
  • One source of truth for each business entity.
  • Events instead of direct calls where modules should react, not control each other.

Chapter 4. Design Around the Business Domain

Why technical layers do not last

Over 10 years, almost everything technical changes in a typical project:

Layer What changes
Programming language PHP → Java → Go → something else
Database MySQL → PostgreSQL → distributed storage
Framework Symfony → Laravel → NestJS → "the next hype"
UI jQuery → React → "server components" → unknown
Infrastructure bare metal → Docker → Kubernetes → serverless

A structure of "controllers / services / repositories / models" does not survive team and technology turnover because it says nothing about the business. A new developer sees folders — and cannot tell where "order" ends and "warehouse" begins.

Business entities live longer

The problem domain changes more slowly:

  • Customer — was, is, and will be (perhaps with different fields).
  • Order — same essence: who, what, when, for how much.
  • Invoice — money, taxes, payment terms.
  • Shipment — from where, to where, when, status.
  • Employee — roles, permissions, org structure.

Ten years ago orders were placed by phone. Today — through an app and a marketplace. An order is still an order. Channels and rules changed — not the essence.

The domain outlives technology

The practical principle: architecture should be built around the problem domain, not technical layers.

Bad (technical layers):            Good (domain modules):

src/                               src/
  controllers/                       orders/
  services/                            OrderService
  repositories/                        OrderRepository
  models/                              OrderStatus
  utils/                             billing/
                                       InvoiceService
                                     inventory/
                                       StockMovement

The orders module can be rewritten from PHP to Go without touching billing. The inventory module can be extracted into a separate service when load grows. Boundaries match business language — understood not only by developers but by product owners and analysts.

Domain-Driven Design here is not a "fashionable methodology" but a longevity tool: bounded context, ubiquitous language, explicit aggregates — all about code and product conversations speaking the same language for a decade.


Chapter 5. Never Make the System Fully Universal

The most expensive trap

The desire to anticipate every variant upfront is one of the most expensive mistakes in enterprise development. It arrives under respectable names: "flexible architecture," "platform," "process builder," "configuration without programmers."

On a slide deck it looks like competitive advantage. In operation — like a monster nobody can maintain.

How monsters appear

Typical symptoms of universality:

Pattern Promise Reality after 5 years
Builders of everything "Any process without code" 400 workflow steps; nobody knows why the invoice was not issued
Configurable fields for everything "The client adds a field" EAV, reports through VIEWs, DBA under constant stress
Universal tables "One model for all documents" 180 nullable columns; migrations are a nightmare
Universal forms "Form builder in admin" Runtime validation; bugs only in prod
Universal processes "BPM for any scenario" Two parallel processes on one document; race conditions

Why it looks good at the start

Universality really saves time in the first months:

  • No need to write a separate model for each document type — "add a type to the catalog."
  • Demo impresses: "watch, we created a new entity in five minutes."
  • Sales promise "fits any business."
  • The architect gets approval for a "scalable solution."

This is a loan at high interest. Interest accrues in database queries, abstraction layers, developers' heads — and in business downtime when a "simple change" takes a month.

Why it becomes a nightmare after a few years

  1. Performance. Universal queries do not index as well as typed ones.
  2. Loss of typing. Errors reach prod — "the field was a string but should be a number."
  3. Refactoring becomes impossible. Logic is spread across metadata, configs, and a "universal engine."
  4. Dependence on "wizards." Only two people understand how the builder works — both have left.
  5. Business loses control. "We can't change the process — it will break three clients."

Rule: 80% of the system is a typed domain with explicit rules. 20% is controlled extension points (plugins, webhooks, custom fields at the periphery). Not the other way around.

For more on the cost of universality in ERP, see The Most Expensive Mistake in ERP Architecture.


Chapter 6. Data Matters More Than Code

Code can be rewritten

Code is an interpretation of business rules in a specific language and framework. It can be:

  • rewritten in another language;
  • split into microservices;
  • replaced by AI generation (a joke, but the trend is clear).

Data is business history. Ten years of orders. Payments. Audit trail. Agreements with clients recorded in the system.

Data is almost impossible to rewrite

Data migration is the most expensive and risky operation in a system's life:

  • Data loss = lawsuits, fines, lost trust.
  • Incorrect migration = reports do not reconcile; accounting panics.
  • Long migration = months of running two systems in parallel.

A bad data model survives any amount of code refactoring — because "touching the database is scary."

Data design mistakes

Poorly thought-out relationships

Foreign keys to a "universal" table without understanding lifecycle. Cascade delete that wipes half the history. Many-to-many without an intermediate entity with metadata.

No change history

"We'll just update the status field." A year later nobody knows who moved an order to "cancelled" and when — yet that is needed for a client dispute or audit.

Business logic stored in data

A skip_validation = true flag. Magic number in type = 7. JSON rules interpreted by a "universal engine." Logic in data is not versioned and not tested like code.

Universal EAV structures

-- Pretty on a diagram. Nightmare in SELECT.
entity_id | attribute_code | value_text | value_number | value_date
----------+----------------+------------+--------------+-----------
1001      | customer_name  | Acme Corp  | NULL         | NULL
1001      | amount         | NULL       | 15000.00     | NULL
1001      | due_date       | NULL       | NULL         | 2026-07-01

Three rows instead of one typed record. Report "all orders with amount > 10,000" — pivot and prayer.

How to design data for a decade ahead

  1. The model reflects the business, not the UI. Table orders, not screen_form_data.
  2. Append-only history for critical entities: audit log, event sourcing for statuses, valid_from / valid_to for reference data.
  3. Explicit types and constraints in the database — not null, check constraints, enums where the domain is stable.
  4. Migration strategy from day one — tool (Flyway, Liquibase, Alembic), naming rules, rollback plan.
  5. Separate operational and analytical data — do not run OLAP queries on production order tables.
  6. Document invariants — "line items sum = total," "cannot delete a paid invoice" — in ADRs and constraints where possible.

Chapter 7. Build the System from Independent Modules

What a real module is

A module is not a folder in the project.

A module is not a namespace.

A module is not a set of files with a common prefix.

A module is a part of the system you can change independently — with minimal risk to the rest and a clear outward contract.

Module test: "Can we replace the Billing module implementation without recompiling Orders?" If not — these are not modules but components of one monolith with an illusion of structure.

Signs of a good module

Sign What it means
Own data The module owns its tables / collections. Others read through an API, not direct SQL
Own rules Business logic is not spread across Utils
Minimal dependencies Depends on contracts, not neighbors' implementations
Explicit public API Clear what can be called from outside vs internal
Independent testing The module is tested in isolation with mocked dependencies

Signs of a bad module

  • Shared logic baseBaseEntity, AbstractDocument that everything inherits from.
  • Shared tables — the Warehouse module writes to the Orders module's tables.
  • Constant cross-calls — A calls B, B calls C, C calls A.
  • Shared database as integration — the "integration via database" anti-pattern.

Modular monolith — a reasonable compromise

You do not have to jump straight to microservices. A modular monolith is one deploy with strict module boundaries inside:

┌─────────────────────────────────────────────┐
│              Modular Monolith               │
│  ┌─────────┐  ┌─────────┐  ┌─────────────┐  │
│  │ Orders  │  │ Billing │  │  Inventory  │  │
│  │ (API)   │  │ (API)   │  │  (API)      │  │
│  └────┬────┘  └────┬────┘  └──────┬──────┘  │
│       │ events     │ events       │         │
│       └────────────┴──────────────┘         │
└─────────────────────────────────────────────┘

When a module matures, it can be extracted into a separate service without rewriting the domain. The boundaries already exist.


Chapter 8. Make Architecture Understandable to New Developers

People change faster than technology

Over 10 years on a typical team:

  • The architect leaves — the one who "designed everything."
  • The team lead leaves — the one who knew all the workarounds.
  • The team turns over two or three times completely.
  • Juniors arrive who become mid-levels in a year — and will make architectural decisions.

The system must outlive its creators. If only one person understands how the payment module works — that is not architecture; that is bus factor = 1.

Signs of understandable architecture

  • Obvious rules. "Orders do not import Billing directly — only through BillingPort." One rule, one place in documentation.
  • Consistent decisions. All modules structured the same: domain / application / infrastructure. Not "here this way, there that way because Pete wrote it."
  • Simple code navigation. A new person finds in a day where orders are created, where status changes, where invoices are generated.
  • Minimal magic. No framework that "injects everything" without understanding. No code generation nobody reads.

The cost of "clever" solutions

Complex patterns are not a sign of maturity. Often they signal an architect showing off:

"Clever" solution Hidden cost
Generic repository for everything Lost typing, opaque queries
Event sourcing everywhere Complexity without CRUD payoff
CQRS for a three-field form Two stores, eventual consistency for a checkbox
Plugin system on day one Nobody writes plugins; everyone hacks the core

Simplicity is not primitiveness. Simple architecture understood by a team of eight will outlive "brilliant" architecture understood by one.

Rule: if you cannot explain the decision to the product owner in five minutes — it is too complex for current scale.


Chapter 9. Document Decisions, Not Code

Documentation that actually helps

Code does not explain itself — it explains what the system does. Not why.

Three years later nobody remembers why PostgreSQL was chosen over MongoDB. Why order status is an enum, not a string. Why bank integration is synchronous, not through a queue.

Useless

  • Comments on every method// returns user by id above getUserById.
  • Describing the obvious — "this class represents an order."
  • Wiki nobody updates — outdated documentation is worse than none.

Useful

  • Why the decision was made — context, constraints, deadline pressure.
  • What alternatives were considered — and why rejected.
  • What constraints exist — "cannot change ID format — external integrations depend on it."
  • What happens if you break the rule — "direct access to the orders table from the reports module will break locking."

Architecture Decision Records (ADR)

An ADR is a short document for each significant architectural decision:

# ADR-007: Order status as enum in the database

## Status
Accepted

## Context
Need to capture order lifecycle. Considered string,
JSON in metadata, separate transitions table.

## Decision
PostgreSQL enum order_status + order_status_history table
for audit.

## Consequences
+ Typing, simple queries
+ Explicit history
- New status = migration (acceptable: statuses change rarely)

ADRs live in the repo next to code (docs/adr/). They are versioned; every developer sees them during onboarding. This is the project's architectural memory — what survives when people leave.


Chapter 10. Design for the Team of the Future

In 10 years the developers will be different

They will not know:

  • Why it was done this way — "that's how it evolved" is not acceptable.
  • What constraints existed — "we needed a release for Black Friday 2023."
  • What trade-offs were made — "we knew EAV was bad but the client paid for a demo in two weeks."

If this is not recorded — the future team either fears changing code or breaks the system trying to "do it right" without context.

Architecture should explain itself

Practical principles:

Principle Example
Predictable structure Every module: domain/, application/, infrastructure/
Minimal magic Explicit dependency registration, not "auto-scan everything"
Maximum transparency Logging at module boundaries, trace id through the request
Convention over configuration One way to do things — not five
Fail fast Contract violation — error at startup, not in prod on Friday

Onboarding test: a new developer in two weeks (not two months) can:

  • find where a typical task belongs;
  • understand which modules a change will touch;
  • write code that passes review without "we don't do it that way here."

If onboarding takes months — architecture is already aging, even if the code "works."


Chapter 11. How to Tell Architecture Is Aging

Early symptoms

Architectural aging is not an event but a process. You can spot it through metrics and team behavior.

Every new module is harder than the last

The first module — 500 lines. The fifth — 5,000 because "we need to cover all cases" and "reuse" a half-dead CommonCore.

Every change requires edits in several places

User story "add a field to the customer card" becomes a task touching 13 files in 4 modules.

Development time grows with the same team size

Velocity drops year over year. Story points rise. "We used to do it in a sprint" — "now it takes a quarter."

Bug count increases

Regressions. Hotfixes after every release. "We didn't touch that module" — "but it broke."

The team fears refactoring

"Don't touch it — it works." "We'll work around it." "One more if." Technical debt grows because paying it down seems more expensive than another workaround.

What to do

  • Measure cost of change — how many files / modules a typical feature touches.
  • Architecture fitness functions — automated checks: "module A must not import module B directly."
  • Regular architecture reviews — quarterly, not "when it's already on fire."
  • Local refactoring — not a "big rewrite" but constant boundary improvement.

Chapter 12. Practical Checklist for a 10-Year System

Use as a design review checklist or when auditing an existing system.

Architecture

  • Clear module boundaries — drawn, documented, checked in CI
  • No global dependencies — no GlobalContext, AppState, "convenient" access to everything
  • No universal entities for universality's sake — every entity justified by the domain
  • Integration through contracts — APIs, events, ports — not shared DB
  • Modular monolith or deliberate services — not a "distributed monolith"

Data

  • Model reflects the business — tables and entities speak domain language
  • Migration strategy exists — tool, process, rollback
  • Change history exists — audit log / event log for critical entities
  • Constraints in the database — not validation in code only
  • OLTP vs analytics separation — or a plan for it

Team

  • New developer can get oriented in weeks, not months
  • Architectural decisions documented — ADRs, module READMEs
  • Shared conventions — structure, naming, code review checklist
  • Bus factor > 1 — at least two people know critical modules

Evolution

  • Changes are localized — typical feature touches 1–2 modules
  • New features do not require rewriting old ones
  • Metrics for cost of change exist — and do not grow unchecked
  • Refactoring is part of the process, not "someday later"

Chapter 13. What Actually Makes a System Long-Lived

Not technology

A system does not live 10 years because you chose:

  • the "right" programming language;
  • the "best" database;
  • the "trendy" framework.

All of these change. COBOL and mainframe systems still run — not because of the technology but because of resilience to change (or because changing is scarier than enduring).

The ability to change

A system lives long because each next change stays cheap enough:

  • the business can adapt to the market;
  • the team can ship features without quarter-long "one button" projects;
  • data is preserved and accumulates value;
  • new people can join without half a year of training.

That is the main criterion of good architecture. Not diagram beauty. Not pattern count. Not matching the latest ThoughtWorks blog post.

The cost of a typical change after 5 years should be comparable to after 1 year — higher perhaps, but not an order of magnitude.

If after three years a "simple" feature costs ten times more than at the start — architecture has already lost. Technology is not the reason.


Conclusion

Architecture is not the art of predicting the future. Nobody knows whether your product will become a marketplace, a SaaS platform, or a niche tool for one industry.

Architecture is the art of reducing the cost of future mistakes:

  • design for change, not only features;
  • fight coupling — the main killer of systems;
  • build around the domain, not technical layers;
  • do not make the system universal — that is the most expensive trap;
  • protect data — it outlives any code;
  • split into independent modules with explicit contracts;
  • make architecture understandable to people you have not hired yet;
  • document decisions — ADRs, not comments on every line;
  • watch for aging symptoms — fix boundaries, do not wait for the "big rewrite."

You cannot build a perfect system the first time. But you can build one where mistakes do not compound exponentially — where each sprint does not make the next one more expensive.

That is what separates long-lived systems from projects rewritten every few years — again and again, each time promising "this time we'll do it right."

Ask yourself not "how do we ship the MVP faster" but "what will it cost to change this in three years." The answer to that question is architecture built to last a decade.