Here's something counterintuitive: the more features you add to your system, the faster it rots. At least that applies if your system is state-based.
Not because of bad code. Not because of bad developers. It happens because of something fundamental about how we design systems.
The Pattern I've Seen Everywhere
I've worked on countless projects over the years. Different domains, different teams, different tech stacks. But the same story plays out every time: active development accelerates decay.
The more you build, the worse it gets.
Most teams think they're doing something wrong. They blame their architecture choices, their testing practices, their documentation. They think if they just worked harder, planned better, refactored more often - the problem would go away.
They're wrong.
The rot isn't a bug. It's a fundamental property of state-based design.
The Core Problem: State as Frozen Understanding
State represents your understanding of the system at ONE point in time.
Your database tables. Your domain model. The contexts you've carved out. They're all snapshots - frozen representations of what you understood about the business when you built and designed them.
But here's the thing: business requirements evolve. Your understanding changes. You learn new things about the domain.
And state-based systems can't adapt easily.
Sure, you can make small adjustments. Add a nullable column here. Change a field type there. Tweak a relationship. But you can't fundamentally change how the system operates. Not without investing massive amounts of time and budget to restructure everything. Both of which you never have in a project.
The Unplanned Requirement: Where Rot Accelerates
The worst is when a requirement comes that you didn't plan for. And it always does.
The first time I saw this and clearly noticed the pattern was on an e-commerce project. We had a clean product model - nice normalized tables, clear domain boundaries. Everything made sense.
Then they wanted to sell new product types. Physical products, sent out with every order. Each type had completely different attributes.
The solution seemed obvious: add some new columns, some properties there. Some products have technical details, some don't. Some have shipping dimensions, some don't. Some have download links, some don't.
It looked innocent at first.
The Real Rot: The Gap Between Models
But here's what happens with those nullable columns: suddenly your domain model in code looks different from your persistence model.
Now you need to understand TWO models. And you need to keep them in sync manually.
And even though your Object-Relational Mappers seem to help at first, they make matters worse.
Even more worse than that - you can't enforce your business rules at the data level anymore. Some products NEED technical details. Some can't have them at all. But the database doesn't know that. It just sees nullable columns.
So you push those constraints into your code. Validation logic scattered across services. Tests trying to catch invalid combinations. Implicit rules that developers need to remember.
More cracks in the architecture with every new feature you add.
The DDD Illusion: Failing Slower, Not Succeeding
At some point, I incorporated Domain-Driven Design. I loved it. Bounded contexts. Aggregate roots. Ubiquitous language. The whole nine yards.
And it helped. It absolutely made things better.
But here's what I realized: DDD makes your system "less likely to fail." That's not the same as making it "likely to succeed."
DDD is about communication, making things smaller. Smaller contexts. Smaller models. Smaller blast zones when things go wrong.
All great things. But if you stay in a state-based world, it won't solve your problems for long.
It doesn't change the fundamental (false) assumption that you can find THE correct model. THE right boundaries. THE proper contexts.
It just assumes you can do it at a smaller scale.
The problem is - there is no ONE model for your system. There are many. Even within a single bounded context.
And without the flexibility to change those boundaries rapidly, you'll still fail. Just slower.
The "problem" of course is not DDD, the problem is State.
The Inescapable Trap: A Concrete Example
Let me show you the trap with a real example.
I recently worked on a system for a law firm. They needed to manage cases. Each case involved different types of people:
- Private persons (clients, witnesses)
- Lawyers (from their firm or external)
- Authorities (courts, police, government agencies)
Each person type looked completely different. Private persons have addresses and contact info. Lawyers have bar numbers and specializations. Authorities have jurisdictions and official designations.
So what do you do?
Option 1: One Table with Nullable Columns
You create a single "Person" table. Most fields are nullable because they only apply to certain person types.
Now you have the same problem as the e-commerce example. Your business rules can't be enforced at the database level. A lawyer MUST have a bar number. An authority MUST have a jurisdiction. But the database doesn't know that.
So you scatter validation logic across your codebase. You write tests to catch invalid combinations. You hope developers remember the rules.
Option 2: Separate Tables Per Type
Maybe you're smarter than that. You create three tables: PrivatePersons, Lawyers, Authorities. Each table has exactly the fields it needs. No nullables. Clean structure.
But now you've optimized for ONE use case - creating and updating individual person types.
You've made every other use case harder.
That was THE fundamental problem in all state-based systems I've worked on. It's all trade-offs. As an architect, I was constantly looking for the least problematic one.
Want to list all people involved in a case? Now you need to join three (and many more) tables. Want to search across all people? Three queries, then merge the results. Want to count how many people are on a case? Sum across three tables.
Expensive queries. Complex logic. More room for bugs.
The Attempted Escape: Multiple State Representations
Maybe you think you can have your cake and eat it too. Keep the normalized structure for writes, but create denormalized views for reads.
Materialized views. Read models. CQRS patterns.
But you've just created a NEW trap. You just distributed your State.
Now those representations have to stay in sync. Constantly. If they drift, your system gives different answers depending on which representation you query.
And you're still coupled. Change the underlying tables, and you might break all the views that depend on them. So instead of giving you freedom to adapt, it gives you just a slightly longer chain. Add a new person type, and you need to update the normalized structure AND all the denormalized views.
You're still trapped.
The Fundamental Truth
Here's what I've learned after seeing this pattern repeat across dozens of projects:
No amount of clever design solves this. Never.
It's not a skill problem. It's not an experience problem. It's inherent to state-based design.
In a state-based system, you're forced to choose ONE physical structure for your data. That structure optimizes for certain operations while penalizing others.
There is no perfect solution. There's only trade-offs.
You can make the trade-offs smaller with DDD. You can make them more manageable with good design patterns. But you can't eliminate them.
The fundamental assumption is wrong. There is no single representation that works. Not even within the perfect designed bounded context.
What I Realized
DDD makes things smaller. It reduces the likelihood of catastrophic failure (by a lot if done right). It gives you smaller, more manageable problems.
Instead of every new requirement affecting the whole system, you now have some requirements affecting some parts of the system.
But without the flexibility to rapidly change your fundamental models as your understanding evolves - without the ability to serve some use cases without compromising all others - you're still on a path to eventual failure.
Just a slower one.
Important: I don't say you can't build state-based systems and maintain them over time. Beautiful and massive systems have been built that way. I say that the longer you work on these systems, the more time and money you need to allocate to keep these systems maintainable. Flexibility is not baked into the architecture.
I don't want to design systems that are less likely to fail. I want to design systems that are likely to succeed. How about you?
Watch the Video
There is also a video on YouTube available covering these concepts.
Next Steps
Ready to explore an alternative approach? Learn how Event Modeling provides the flexibility to serve multiple use cases without the trade-offs inherent in state-based design.
Want to learn how to apply Event Modeling and Event Sourcing in practice?
Follow the Online Course “Implementing Eventsourcing” - comes with a Lifetime Event Modeling Toolkit License.