WHY SIMPLE ARCHITECTURE WINS
You’ve seen it happen. A team spins up a Kafka cluster for a basic CRUD app. Another implements twelve microservices where two would suffice. A third builds a custom CI/CD orchestrator when GitHub Actions would have sufficed. I’ve been guilty of this myself—optimising for problems that exist only in my imagination.
Here’s the uncomfortable truth: most over-engineering stems from a combination of resume-driven development, fear of future constraints, and the seductive appeal of complex systems. We build for Netflix-scale traffic when we’re handling hundreds of requests per day. We design for eventual consistency when our data would fit comfortably in a spreadsheet.
After reading this, you’ll recognise the signs of architectural over-engineering, understand when complexity is genuinely warranted, and save yourself months of development time on projects that don’t need it.
The Three Deadly Myths That Drive Over-Engineering
Let’s start by dismantling the three most common justifications for unnecessary complexity.
Myth 1: “We’ll need to scale later, so we should design for it now.”
This is premature optimisation in its purest form. As Knuth famously said, “Premature optimisation is the root of all evil.” Yet we keep building distributed systems for applications that might—if everything goes perfectly—reach a fraction of the scale that requires them.
The reality? Most applications never face the scaling challenges their architects anticipate. You’re optimising for a problem you don’t have, and the complexity you’re adding now will slow down feature development by 40-60%. When you actually need to scale, you’ll be bottlenecked by the very complexity you added early.
Myth 2: “Microservices will make our team faster.”
Microservices promise autonomous teams and independent deployments. What they actually deliver, especially for small organisations, is distributed complexity. You’ve traded local complexity for remote complexity, and you now need to deal with network failures, eventual consistency, and deployment orchestration.
For a team of fewer than ten engineers, microservices are almost always the wrong choice. A monolith deploys in 5 minutes; a twelve-service system takes 45+ minutes. The coordination overhead exceeds the benefits. You end up with a distributed monolith—worse than a proper monolith because at least a monolith is coherent.
When microservices do make sense for small teams: There are genuine exceptions. A team of five might benefit from microservices when you have truly independent domains with different scaling characteristics—say, a real-time video transcoding service that needs GPU instances alongside a lightweight CRUD API. Or when you’re integrating with third-party systems that require isolation for security or compliance reasons. Or when different parts of your system genuinely need different technology stacks—a Python ML pipeline alongside a Rust performance-critical component.
The key question isn’t “should we use microservices?” but “do we have a forcing function that makes the operational overhead worthwhile?” If the answer is “we might need to scale independently someday” or “it’s more modern,” you don’t have a forcing function—you have speculation.
Myth 3: “Complex architectures demonstrate technical sophistication.”
I’ll be direct: nobody cares about your clever use of event sourcing, CQRS, or custom orchestrators. They care whether your application works, is reliable, and solves their problem. Technical sophistication without business value is just self-indulgence dressed up as engineering.
Recognising the Signs of Over-Engineering
How do you know if you’re over-engineering? Here are the warning signs I’ve encountered time and again:
Your infrastructure diagram needs a zoom button.
You have message queues, caches, databases, search indexes, and CDN layers—all for an application that serves a few hundred users. When I ask you to explain the data flow, you need five minutes and three whiteboard diagrams. A single user request passes through six services before returning a response. You’ve built complexity for the sake of complexity.
You’ve solved problems you don’t have.
You’ve implemented circuit breakers for services that have never failed. You’ve built horizontal auto-scaling for traffic that never materialises. You’ve designed multi-region failover for an application where downtime would be an inconvenience, not a catastrophe.
Your team spends more time maintaining the architecture than building features.
If your engineers spend more time debugging distributed system issues than delivering user-facing functionality, your architecture is failing. Microservices introduce 10-100x more operational overhead than monoliths. The purpose of software is to deliver value, not to provide interesting problems for engineers to solve.
You can’t deploy to production in under an hour.
Complex architectures have complex deployment pipelines. If you can’t push a fix to production quickly, you’ve built yourself a fragility trap. The more moving parts, the more things that can break.
You can’t test a change without spinning up half the infrastructure.
When a simple bug fix requires mocking three services, two message queues, and a cache layer, you’ve lost the ability to iterate quickly. A monolith lets you run tests locally; your distributed architecture requires a staging environment that takes twenty minutes to provision.
The “Netflix Scale” Litmus Test
Before you add that next layer of complexity, ask yourself these questions:
Does this solve a problem we have today, or one we might have in the future?
- Today’s problem: implement it.
- Future problem: wait until you have evidence it will actually occur.
What is the cost of this complexity in engineer-hours?
- Not just initial implementation—ongoing maintenance, debugging, and cognitive load.
- Cloud services often cost more than they appear, but engineer time is the real bottleneck.
Could we solve this with a simpler approach?
- The answer is almost always yes.
- Amazon Prime Video cut infrastructure costs by 90% by replacing distributed components with a monolith.
- Start simple. Complicate later when forced.
What happens if this component fails?
- If the answer involves complex failover logic, you’re building fragility.
- Microsoft’s Azure Front Door outage in 2025 showed how a configuration error in a complex global routing system cascaded into worldwide service disruption.
- Simpler systems fail in simpler ways.
When Complexity Is Actually Warranted
I don’t want you to think all complexity is bad. There are genuine cases where sophisticated architecture is necessary:
You have demonstrable scale.
You’re handling 10M+ requests per day. Single-node performance is a genuine bottleneck. You’ve exhausted connection pooling, query optimisation, and vertical scaling—and you have the metrics to prove it.
You have regulatory requirements.
You need to isolate sensitive data for compliance reasons. You must maintain audit trails. You need to demonstrate data residency.
You have a large team that requires bounded contexts.
You have 50+ engineers working on a single product (not 10). Monolithic deployment cycles are taking 2+ hours. You need autonomous teams to maintain velocity.
Notice the pattern: these are problems you actually have, not hypothetical ones.
The Modular Monolith: The Middle Ground Nobody Talks About
The conversation shouldn’t be “monolith vs microservices”—that’s a false dichotomy. The real spectrum runs from tightly-coupled monolith → modular monolith → distributed services → microservices. Most teams would benefit from stopping at the second step.
A modular monolith gives you the organisational benefits of bounded contexts without the operational overhead of distributed systems. You structure your code into vertical slices—each module owns its domain, has clear interfaces, and could theoretically be extracted into a service later. But you deploy as a single unit.
What you get:
- Clear boundaries between domains (payments, users, inventory)
- Teams can work on modules independently with minimal merge conflicts
- Single deployment pipeline, single database connection, single log stream
- Refactoring across modules is a code change, not a distributed transaction
- When you do need to extract a service, the seams are already there
What you avoid:
- Network latency between service calls
- Distributed tracing complexity
- Container orchestration overhead
- Service discovery and registry maintenance
- The “microservices premium” on every debugging session
For a team of five building a product that might need to scale, the modular monolith is almost always the right answer. You get the clean architecture benefits without paying the distributed systems tax. If you hit genuine scaling constraints later, you extract the bottleneck module into its own service—because you’ve already defined the interface.
Practical Steps to Avoid Over-Engineering
Here’s a framework I use to keep myself honest:
Start with a modular monolith.
Not a ball of mud, and not seventeen services. Structure your monolith with clear module boundaries from day one. Use separate packages or folders per domain. Define explicit interfaces between modules. Keep database schemas logically separate even if they’re in the same database. You’ll thank yourself later—whether you stay monolithic or eventually extract services.
Use managed services until they hurt.
Don’t run your own Kafka cluster. Don’t self-host your database. Use the managed offerings from your cloud provider. They’re not always cheaper, but they’re always simpler, and your time is more expensive than the cloud bill.
Measure before you optimise.
You can’t know if you need a cache until you measure database performance. You can’t know if you need a CDN until you measure actual latency. Optimise based on data, not speculation.
Do the simplest thing that could possibly work.
Then ship it. Measure it. Optimise only when you have evidence that the simplest approach isn’t sufficient.
Recovering from Over-Engineering
If you’re already deep in the complexity trap, here’s the way out:
Stop adding complexity immediately.
Freeze new architectural initiatives. No new services, no new queues, no new databases. Focus entirely on delivering user value with what you have.
Identify the core value proposition.
What does your application actually do? Strip away everything that doesn’t directly contribute to that core value.
Consolidate where you can.
Start by identifying one service boundary that adds complexity without value. Move that functionality back into the main application. Remove the message queue between two tightly-coupled services. Each consolidation reduces deployment overhead, debugging time, and cognitive load.
Accept technical debt strategically.
Sometimes you need to take a shortcut to get out from under an architecture that’s crushing you. That’s fine. Pay it down later when you have capacity.
Further Reading
If you found this useful, you might also enjoy these articles on similar topics:
- Premature Optimization: Stop Over-Engineering (Qt.io, Nov 2025)
- Why Over-Engineering Happens (Oct 2025)
- Monolith vs Microservices: When to Use Each Architecture (Dev.to, 2025)
For more on infrastructure decisions, check out my thoughts on PaaS First: Why 2026 is the End of Defaulting to Kubernetes / or read about when self-hosting becomes its own addiction /.
Start simple. Ship fast. Add complexity only when reality forces your hand.