DEV Community

Michel MAIER
Michel MAIER

Posted on

Accidental Complexity Is Killing Your Backend (And You Invited It In)

Fred Brooks said it in 1986. Nearly four decades later, we're still not listening.

In No Silver Bullet, Brooks split software complexity into two kinds: essential complexity (the irreducible difficulty of the problem you're solving) and accidental complexity (everything else). The first one is your job. The second one is your fault.

Here's the uncomfortable truth: most of the complexity in your codebase is accidental. It didn't come from the domain. It came from a bundle someone added on a Tuesday afternoon, a framework that seemed like the obvious choice, a "let's just use X, everyone does" decision made when the deadline was close and the future felt abstract.

I've spent 17 years on PHP backends. Symfony 2 in 2011 at a travel agency SaaS. Symfony 6 in 2024 at a proptech expanding internationally. The tools changed. The mistake didn't.


The Solution That Becomes the Problem

Accidental complexity doesn't announce itself. It arrives disguised as a solution.

You need user authentication. Someone suggests FOSUserBundle. One composer require and you're done. Six months later you need to customize the user entity in a way the bundle didn't anticipate. You start overriding templates. Extending classes. Fighting conventions. Two years later you're still on Symfony 3 because the bundle's dependency tree makes upgrading a two-week project nobody has budget for.

The bundle solved a problem on day one. It became the problem on day 400.

This isn't a PHP problem. Every ecosystem has it. But PHP's history makes it unusually visible. The ecosystem went through a period where third-party bundles filled genuine framework gaps, and those bundles now live as legacy dependencies in thousands of codebases. Not because they were bad. Because the framework absorbed what they did, and nobody had the courage to rip them out.

FOSUserBundle. FOSRestBundle. JMSSerializer. Sonata Admin. Tools that answered a real need at a specific moment. Tools that projects are still dragging around years after Symfony made them irrelevant.


When the Tool Starts Driving

There's one category of accidental complexity I find more damaging than the others: when a tool designed to save time starts shaping your domain model.

API Platform is the obvious example. Annotate an entity, get a documented REST API with pagination, filtering, and validation. For CRUD-shaped domains, it delivers fast.

But here's the thing about domains: they don't stay CRUD-shaped.

The moment your business logic gets complex (state machines, conditional permissions, operations that don't map to "update a record"), API Platform starts pushing back. State Processors become the only acceptable home for business logic. Your domain model starts bending toward what the tool expects. You spend your sprints figuring out how to satisfy the framework rather than solving the actual problem.

That's the signal. When every new requirement triggers "how do we do this in API Platform" instead of "what does the domain need here", you've lost control of your architecture.

The accidental complexity isn't in the code you wrote. It's in the constraint space you accepted when you let the tool drive.

Sonata Admin follows the same pattern with an extra layer of irony. It was introduced to avoid writing CRUD. And it works, until the CRUD needs one exception. Then another. Then a custom workflow. At that point you're writing more code than if you'd started from scratch, in a more constrained environment, fighting abstractions that weren't designed for where you ended up.

And then Symfony ships a new major version, and you discover that Sonata's dependency cascade turns upgrading into a negotiation.


The Framework-In-The-Framework Trap

Both of those examples point at the same underlying problem: you adopted a second framework on top of your first one.

Not a library. A framework. With its own conventions. Its own vocabulary. Its own opinions about how your code should be organized. It sits on top of Symfony and interacts with it in ways that are sometimes documented and sometimes discovered the hard way. Usually in production. Usually at the worst moment.

The compounding effect is where it gets ugly. Your team now has to understand Symfony, and API Platform, and how they interact, and how your domain maps onto both. Every new developer carries that cognitive overhead from day one. Debugging requires crossing abstraction layers. Onboarding takes twice as long.

This is where the essential/accidental distinction gets concrete. The complexity of your business domain is essential. You can't simplify it without simplifying the problem. The complexity of three abstraction layers talking to each other is accidental. You chose it. You can choose differently.

The alternative isn't always obvious in the moment. Plain Symfony controllers feel "too low-level." Writing admin interfaces from scratch feels wasteful. But the cost of that initial investment is almost always lower than the cost of fighting a mismatched abstraction for three years.

When I work on PHP/Symfony backends, I reach for Symfony's native components first. Messenger for command dispatching. Security, Serializer, Validator. Plain controllers with explicit command handlers. EasyAdmin when the administration really is simple and is likely to stay that way. The moment requirements suggest otherwise, vanilla wins.


The Slow Accumulation

Beyond individual tool choices, there's a subtler form of accidental complexity that's harder to see: dependency inertia.

Projects don't become complex overnight. It happens through a series of individually reasonable decisions, each one adding a small layer of indirection, a small external constraint, a small deviation from the simplest possible path.

A library gets added to solve a specific problem. The problem evolves, the library no longer fits, but removing it would require touching code nobody fully understands. So it stays. Another dependency gets added that happens to conflict with the first. Workarounds accumulate. The upgrade path narrows.

No single choice is the culprit. The culprit is the compound effect of many choices made without a coherent architectural vision.

And accidental complexity compounds. Essential complexity stays roughly proportional to the domain. Accidental complexity grows faster: each new layer interacts with all the previous layers, multiplying the surface area of things that can break.

The practical result: every change carries disproportionate risk. Developers get cautious. Features take longer than they should. Sprint after sprint, the team's energy goes into navigating existing complexity rather than solving new problems. You're paying compound interest on decisions made years ago by people who aren't on the team anymore.


What Actually Helps

"Simplify your stack" is easy advice to give and hard to apply. Let me be more specific.

First: make the complexity visible. Most teams know their codebase is complex. Few have a clear map of where the accidental complexity lives. Which dependencies are actively problematic. Which abstractions are fighting the domain. Which upgrade paths are blocked and why.

Static analysis tools (PHPStan, Psalm) help. Honest architectural reviews help more. Not to add process. To see clearly.

Second: separate what you can remove from what you can only manage. You can't always eliminate accidental complexity immediately. Sometimes the right move is to isolate it. Wrap the problematic dependency behind an interface. Contain the damage. Plan the migration.

This is the strangler fig pattern applied to your own codebase. The old thing keeps running. The new thing grows around it. You migrate incrementally, delivering value the whole time, without a big-bang rewrite that requires freezing all feature development for six months.

Third (and this is the step most teams skip): design to resist future accidental complexity. Hexagonal architecture isn't a style preference. The point of separating the domain from its infrastructure is that the domain stays clean regardless of what happens in the outer layers. Swap the ORM. Migrate the API format. Replace the messaging system. None of it touches the code that contains your business rules.

Brooks was right that there's no silver bullet. But hexagonal architecture creates a structural barrier against the most common source of accidental complexity: infrastructure concerns leaking into the domain.


The AI Angle Nobody Is Talking About

There's a new dimension to this problem most teams haven't processed yet.

AI coding assistants are powerful on well-structured codebases. They understand domain logic when the domain is clearly expressed in the code. They suggest accurate refactorings when the boundaries are explicit. They generate tests that reflect real business rules when the business rules are in the right place.

On a codebase saturated with accidental complexity, business logic scattered across controllers, ORMs, bundle hooks, and admin configurations, AI assistance degrades fast. The model can't reason about what the code means because the code doesn't express meaning clearly. It produces generic suggestions, misses domain-specific invariants, and adds complexity instead of reducing it.

Accidental complexity doesn't just slow down your team. It degrades the AI tools that are supposed to help them. Clean architecture used to be a code quality argument. Now it's also an argument about whether your team can actually leverage the tools reshaping how software gets built.


The Honest Conclusion

Accidental complexity charges compound interest. Every sprint spent navigating poorly-fitting abstractions is a sprint not spent on the domain problems that actually matter.

The fix isn't glamorous. Reach for native framework components before third-party bundles. Resist the tool that promises to do more until you're sure "more" is what you need. Draw explicit boundaries between your domain and your infrastructure, and hold them under pressure.

Slower in week one. Significantly faster in year two.

Brooks was writing about silver bullets. But the real point of the essay is quieter: there's no shortcut around the essential complexity of what you're building. The only question is how much accidental complexity you choose to carry alongside it.

Most teams carry far more than they realize.


Senior backend freelance developer specializing in PHP/Symfony, clean architecture and legacy modernization. php-freelance.com

Top comments (0)