The Wrong Measure
Software doesn't fail when it stops working. It fails when the cost of keeping it working exceeds what anyone is willing to pay.
That distinction sounds simple. Its consequences are not.
The industry measures software by whether it works. Delivered on time, passes the tests, satisfies the requirements — success. The team moves on. The architecture gets praised, the approach gets repeated, the pattern gets adopted elsewhere. Nobody measures the cost of keeping it working six months later, three years later, after two team changes and four rounds of new requirements. That cost exists. It is often large. It is almost never attributed to the decisions that caused it.
This is the central problem of software development, and it has a name: unfalsifiability. There is no comparable version of the same application, built differently, to measure against. The messy system that shipped is always beating the elegant system that wasn't built. The working application is always beating the hypothetical better one. You cannot prove a different approach would have been cheaper, because that approach was never taken.
If it works, it's a success. And that is where all the trouble starts.
You Don't Pay for Complexity When You Build It
You pay for it every day afterward — and the invoice arrives without a line-item explanation.
A framework that saves three weeks of initial development might cost three weeks a year in version upgrades, security patches, and breaking changes. Spread across five years, across a team of six, that initial saving is long gone. But nobody connects those upgrade sprints to the original decision to adopt the framework. The cost is real. The attribution is absent.
This is what unfalsifiability does to cost. It doesn't make costs disappear — it makes them untraceable. The expense shows up as "enterprise complexity," as "technical debt," as "that's just how large systems work." It is rarely traced back to an architectural decision made three years ago by people who are no longer on the team.
Consider what is actually being counted when someone says "we chose this framework to move faster." They are counting the lines of code they wrote. They are not counting the lines of code they are now responsible for — the framework itself. Those lines execute. They have bugs. They have CVEs. They have opinions about how your application should be structured, encoded in defaults and conventions that were answered before you understood your own domain.
A domain-focused implementation without a heavyweight framework is typically smaller in raw lines of code than the template and configuration code required to set that framework up. Before a single line of business logic is written. Add the framework's own codebase — the code being executed on every request — and the surface area for bugs, security vulnerabilities, and maintenance burden has expanded by an order of magnitude. For what? For the privilege of not writing code yourself.
A Ferrari is faster than a tractor on every measurable dimension. It is also completely useless in a field. And it costs more to buy, more to insure, more to service, and requires specialists to repair. Every dimension of cost is higher, for a vehicle that performs worse at the actual job. The sophistication is not the problem. The mismatch is the problem. And unfalsifiability means you never have to confront the mismatch directly — the Ferrari is technically moving across the mud, but you can't see how much damage it's doing to the soil, or how much you're spending on replacement clutches.
Know What You Are Doing
Fred Brooks gave the industry a precise vocabulary for this problem fifty years ago, and the industry has largely ignored it.
Essential complexity is the complexity intrinsic to the problem itself. It cannot be removed. It is the business rules, the domain constraints, the lifecycle of an order, the eligibility rules for a customer, the regulatory requirements of a financial product. This complexity exists whether you model it or not. The business is as complex as it is.
Accidental complexity is everything else. The frameworks, the indirections, the patterns applied without cause, the services that exist because nobody decided where the behavior actually belonged. Accidental complexity is not intrinsic to the problem. It was introduced by the approach.
The critical implication is this: you can only minimise what you can see. And in most software built today, the essential complexity is invisible — scattered across service classes, duplicated across microservices, buried under framework conventions — while the accidental complexity is everywhere and growing. The map is the problem, not the territory.
Getting this right requires knowing what the application must do. Not what it does — what it must do. The mandatory behavior, the non-negotiable rules, the core of what this system exists to perform. Everything else is optional. Everything else has a cost. And that cost should be justified, explicitly, before it is paid.
The Tractor on the Field
Simplicity is not an aesthetic preference. It is the engineering discipline of not paying for things you don't need.
The simplest solution that honestly expresses what the application must do is not the lazy solution. It is the hardest solution to find, because it requires actually understanding the problem before reaching for the tools. It is also the solution that survives. Not because simple things are inherently more durable, but because simple things are easier to understand, easier to change, and easier to replace when understanding deepens.
Implementation is a learning process. You do not know the domain fully when you begin. You discover it through building, through conversation with the people who own it, through the friction of encoding rules that turn out to be more nuanced than they first appeared. The application you build in month one is not the application the business needs in month eighteen. The question is whether you built something that can become that application, or something that has to be replaced by it.
A minimalist approach — not sparse, not incomplete, but precisely sufficient — is the tractor on the field. Unglamorous. Fit for purpose. Still running in fifteen years. Serviceable by someone who wasn't there when it was built. Modifiable without calling a specialist. Cheap to operate on the days nothing goes wrong, and cheap to fix on the days something does.
The Ferrari has its place. That place is not most software. And unfalsifiability means the Ferrari stays in the field long after it's clear it isn't working, because there's no other field to compare it to.
Make the Essential Complexity Visible
The domain model is not a goal. It is not a purity exercise. It is not an architectural pattern to be applied because the textbook recommends it.
It is a tool for making the essential complexity of the application visible, centralized, and honest.
When the business logic of an Order lives in the Order — when an Order knows what it means to be cancelled, what it means to be fulfilled, what state it must be in before shipment can proceed — that logic is findable. It is in one place. A new developer can locate it. A domain expert can read it and recognize it. A compliance auditor can verify it. When that same logic is scattered across service classes, duplicated in three microservices, and partially encoded in database triggers, it is effectively invisible. It exists. It executes. Nobody knows exactly where it is or whether the three copies agree with each other.
The legibility bar matters here, and it should be set higher than most developers expect. A domain expert who is not a developer should be able to read the core domain objects and understand what the application is doing and why. Not the implementation details — the behavior. What is an Order? What can it do? What does the business enforce at that level? If the answer to those questions requires navigating framework annotations, service orchestrators, and DTO mappings, the essential complexity is not legible. It is hidden. And hidden complexity is expensive complexity, because it has to be rediscovered every time it needs to change.
This is also where consistency becomes structural rather than aspirational. If everything Order-related happens in Order, then contradicting logic between two parts of the system is immediately obvious — because there is only one place to look. In a system where order logic lives in seventeen service methods across four microservices, contradiction is not just possible, it is inevitable. And nobody will notice until a customer finds it.
The domain model is the centralized, codified, documented expression of what the business is. As long as the business continues in the same domain, that model should not have to be rewritten. The framework it runs on can be replaced. The delivery mechanism can change. The infrastructure can evolve. The essential complexity, correctly encoded, is the permanent part. Everything around it is the replaceable part. Getting that boundary right is the engineering challenge. Getting it wrong is what generates the invisible invoice.
Keep Domain Experts Close
You cannot model what you do not understand. And you cannot understand a business domain from requirements documents, user stories, and ticket descriptions alone.
Requirements describe motion through a system — a user does something, something happens. They teach you the rivers. A domain model teaches you the terrain. Without understanding the terrain, you are always following the water, never knowing where you are.
Domain experts — the people who actually own the business processes, who know why the rules are the rules, who feel it when the software gets something wrong — are not stakeholders to be consulted at sprint reviews. They are the source of the essential complexity. The conversation with them is not a requirements-gathering exercise. It is the modeling work itself.
The UI plays a specific role here. Not the polished end-user interface, but an early working interface that makes the domain model visible to the domain expert in a form they can evaluate directly. Two people can use the same word and mean different things. They can agree on a description and disagree entirely on what it describes. That misalignment is invisible in conversation. It is undeniable on a screen. Building something the domain expert can navigate is the fastest way to find out whether the model is honest.
Implementation is a learning process. The model you have at the end of month one is not the model you will have at the end of year one. What you are building is not just software — it is accumulated understanding of what the business actually is. That understanding should be encoded in the model. The model should get more true over time, not more obscure.
Beware of the Hidden Costs
Tools should be selected by one criterion: do they serve the essential complexity, or do they obscure it?
A framework that handles persistence, wiring, or HTTP without imposing opinions about where behavior should live is earning its place. A framework that answers structural questions before you have understood your own domain — that substitutes a recipe for architectural thinking — is introducing accidental complexity from day one. You are paying for its opinions whether you wanted them or not.
The distribution question deserves particular directness. Event-driven architecture, CQRS, and microservices each originated as responses to real problems at genuine scale. Each carries a significant and permanent cost: distributed tracing, eventual consistency management, versioned service contracts, deployment orchestration, network failure handling. And the loss of one guarantee that a single well-modeled application provides for free — transactional consistency. Once operations are distributed across services and event queues, rollback is no longer a database primitive. It is an engineering problem, solved with compensation logic and saga patterns, maintained indefinitely.
There is also a structural cost that rarely gets discussed: distribution freezes your context boundaries. A monolith with a clear domain model can redraw its internal boundaries as understanding of the domain deepens — because learning continues, and the model should move as you learn. Once you have cut service boundaries and built contracts and deployment pipelines around them, that learning is frozen. Every misunderstanding about the domain that was encoded in a service boundary is now a permanent architectural feature. You pay for it in coordination overhead, in contract negotiation, in the impossibility of the refactoring that would have taken an afternoon in a monolith.
These are expensive tradeoffs. They are justified at genuine scale — when one part of the system genuinely needs to scale independently, when teams are large enough that shared deployment is a bottleneck, when the operational investment is proportionate to the problem. For the vast majority of software, they are not justified. And unfalsifiability means they persist anyway, because the cheaper alternative was never built.
The economic threshold is real. Distributed architectures make sense when you have hundreds of millions of rows of hot data, hundreds of thousands of concurrent users, and extreme load skew requiring parts of the system to scale independently by orders of magnitude. Most software never reaches that threshold. Most software pays the distribution tax anyway, and calls it modern.
Longevity Is the Return on Investment
Every argument in this article points to the same place, and the direction of travel is not what most people expect.
When essential complexity is managed — made visible, centralized, and honest — ongoing costs drop. The model is the documentation, so documentation cannot go stale. The logic is in one place, so contradictions cannot accumulate quietly. New requirements find their place in something that already exists, or reveal through the friction of not fitting that the model needs to grow. Either outcome deepens understanding. Either outcome improves the system. Understanding compounds. The software gets easier to work with as it matures, not harder.
When essential complexity is not managed, costs compound. Each new requirement lands on top of whatever was there before, in whatever shape it happened to be in. The essential complexity becomes harder to find, harder to verify, harder to change without touching something else. The team grows but delivery does not improve. The diagnosis is always the same: enterprise complexity, accumulated technical debt, that's just how large systems work. It is rarely diagnosed as what it actually is — the predictable consequence of building without a map.
That is the ROI argument for managing essential complexity, and it is already strong. But there is a third level that the industry almost never discusses, because it inverts the assumption that rigor costs more upfront.
When you understand the essential complexity of a system before you build it, initial development costs drop too.
Not because the work becomes easier. Because you stop doing work that was never necessary. You select the tools the problem requires rather than the tools you already know. You do not adopt a framework whose opinions you will spend years working around, because you can see that those opinions do not fit your domain. You do not distribute a system that did not need to be distributed, because you can see that the transactional consistency you are about to give up is load-bearing. You do not build the service, the saga, the compensation logic, the versioned contract, the deployment pipeline — because you can see that the problem those things solve is a problem you created, not a problem you had.
This produces two diverging cost curves that never cross — because the essential complexity approach was never the more expensive one. It only appeared that way because the costs of the alternative were invisible.
The essential complexity approach starts lower — no unnecessary tooling, no framework opinions to work around, no infrastructure for problems you do not have. Each new function point finds its place in a model that already understands the domain. A new business rule is a method on the object that owns it. The cost per function point decreases over time as understanding compounds and the model absorbs requirements rather than accumulating them.
The non-essential complexity approach starts higher — the framework, the boilerplate, the services, the distribution tax paid before a line of business logic exists. Each new function point adds new services, new mappings, new contracts. Logic that should live in one place gets duplicated across three. The cost per function point increases over time, because every addition lands in a codebase that is slightly harder to understand than it was before. The curve climbs until it hits a ceiling — the point where replacement is cheaper than continued maintenance or extension. At which point the system gets rebuilt. Without the domain model, the rebuild reconstructs the same misunderstandings into the new version, faster and with more confidence. The new system starts higher than the original did, climbs faster, and hits the ceiling sooner.
The Ferrari does not just cost more to run. It costs more to buy. Understanding the domain first means you arrive at the dealership knowing you need a tractor — and you leave without the Ferrari, without its finance agreement, and without the specialist on retainer for the day it breaks down.
Working software is not the asset. The understanding encoded in the software is the asset. A working application without that understanding is a disposable item — it worked when it left the factory, and it was not designed to be serviced.
The tractor is still in the field in year fifteen. The Ferrari is in the shop, waiting for a specialist who knows the model. The tractor cost less on day one, costs less every year, and is still doing the job it was bought to do.
The industry treats rigor as the expensive path. It is the only path that gets cheaper as you walk it.
The invisible invoice arrives eventually. The only question is whether you chose a vehicle designed for the field — or whether someone is still trying to explain why the clutch keeps burning out.
A Note From My Personal Experience
The two cost curves described in this article are not theoretical. I have worked on and maintained two large systems over fifteen and seven years respectively. In both cases the core domain model has never needed to be rewritten. On the second system, the UI was replaced entirely — a complete rebuild — without the domain module being opened. The permanent part stayed permanent. The replaceable part was replaced. Exactly as intended.
I have also introduced this approach into stalled projects at large organizations — systems where delivery had slowed, complexity had accumulated, and nobody could confidently explain where the business logic lived. In each case, making the essential complexity visible and centralized was what unstalled them. Not a new framework. Not a new architecture. Understanding what the system was actually for, encoded in a place everyone could find.
The approach works. It has worked for fifteen years on one system, seven on another, and across multiple recoveries of projects that had lost their way. The cost curve is real. The only question is which one you want to be on.
Top comments (0)