Skip to content
The Art of the 'Perfect' System Architecture: Why Simple is Hard

The Art of the 'Perfect' System Architecture: Why Simple is Hard

The Art of the "Perfect" System Architecture: Why Simple is Hard

Every system has a moment where it almost got designed perfectly. And every one of those moments is the same story: a team drew a beautiful diagram, everyone nodded, and then reality quietly rewrote the diagram over the next two years.

I have killed more good architectures by accident than I have built on purpose. The one lesson that survived every incident, every 2 a.m. page, every angry postmortem, is deceptively boring: simplicity is not the absence of effort. It is the residue of relentless editing. This post is that lesson, unpacked for the engineer who is about to draw a really impressive diagram.

The seduction of complexity

Walk into any architecture review and count how many people are impressed by a ten-box microservice diagram versus a single box that says "app". The ten boxes win every time. Nobody claps for the box.

Complexity has a gravitational pull because it looks like rigor. More boxes means more abstractions, more "layers", more professional vocabulary. Dijkstra saw this in 1984 and said it better than any tweet ever will: "Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better." InfoQ has spent a decade making the same case — simple architectures are easier to communicate, build, deploy, operate and evolve — and yet we keep drawing the ten boxes. Because simple does not mean easy, and the design that looks simplest is often the one that took the most thinking to find. That inversion is the entire topic of this post.

Chapter 1 — The illusion of perfection

The perfect architecture does not exist, because "perfect" is a target that moves as the problem does. What exists is a set of trade-offs you made on a Tuesday afternoon and then forgot you made.

Two failure modes are worth naming before anything else:

1. The Golden Hammer. You have Kubernetes, so every problem looks like a cluster to deploy. You have Kafka, so every data flow looks like a stream. Tools become beliefs, and beliefs are the hardest dependencies to refactor. The tool is never the problem; the belief that the tool is the answer to a problem it was not designed for is the problem.

2. Premature abstraction. We abstract before we understand the repetition. The result is a hierarchy of indirection where a simple POST /orders takes eleven files to trace. Fred Brooks called the move from machine code to high-level languages the single biggest productivity gain in software history — but that gain came from abstraction in the right place. The first sketch of an abstraction is usually a guess.

The abstraction tax

Every layer you add has a cost that does not appear on the diagram. It appears in your debugging time. When a request fails, the work of understanding a system grows with the number of layers it crosses:

MONOLITH TRACE                MICROSERVICE TRACE
─────────────────             ─────────────────────────────
1. app/router → service       1.  client
2. service → db               2.    → api-gateway (auth, TLS)
                              3.      → service-a (discovery)
Then you debug:               4.        → service-b (HTTP + retry)
  a. logic bug                5.          → service-c (event bus)
  b. db query                 6.            → db (per-service)
                              7.        ← 404 (schema drift)
Then you debug:
  a. logic bug      b. db query      c. which hop failed?
  d. timeout? retry? e. ordering?     f. schema drift? g. idempotency?

The monolith fails in two places. The distributed system fails in seven — and every hop has its own timeout, retry, serialization, auth, and "did the event arrive once or twice?" problem. None of that complexity made the user's request faster. It made the diagram prettier.

That is the visibility trade: the parts you can see on a diagram get glorified, while the invisible parts — latency, operational burden, mental load — get billed to the night shift.

Chapter 2 — The hidden tax of over-engineering

Over-engineering is not "too much code". It is paying for capability you are not using, in three currencies.

1. Latency you will never invoice

Every network hop adds real milliseconds. Every extra service adds a TLS handshake, a serialization round, a possible retry. If your monolith serves a request in 40ms and your microservice split serves it in 400ms, you have spent months of team effort to make your product slower for your users. The cloud does not waive the laws of physics because your diagram is fancy.

2. Operational overhead as a subscription

This is the tax most teams miss, because it is paid continuously and quietly. A three-service architecture needs: three deploy pipelines, three log formats, three secrets stores, three health checks, three alert rules, three upgrade schedules, three ways to answer "where is the bug?". MIT Tech Review quotes Thoughtworks on the same trap — every solution, including microservices, redistributes complexity rather than removing it. Microservices solved deployment pain and created operational pain. That is not a bug in the approach; it is the actual deal you signed.

3. Cognitive load on the only resource that matters

Your team's working memory is the scarcest resource in the building. McConnell frames the whole of programming as "an attempt to compensate for the strictly limited size of our skulls" — managing enormous complexity within a brain that evolved for savannas. Every extra module, every extra if, every extra hop is a bill against that limited skull. When an on-call engineer must hold seven services in their head at 3 a.m., you are not running an architecture. You are running a memory experiment with your users as the subjects.

When the box is a lie: the distributed monolith

Here is the expensive one. Teams decompose for agility, but keep a shared database, shared transactions and synchronous calls between services. What they built is not microservices — it is a monolith with worse latency and network failures added. This is the worst of both worlds, and it is remarkably common. The diagnostic is easy: if your services cannot be deployed independently without coordinated releases, you have a distributed monolith, and the distributed-computing fallacies are now your production bugs.

Chapter 3 — The framework of simplicity

Simplicity is not a vibe. It is a decision framework you apply at every level. These are the four that survived contact with production.

1. YAGNI as a discipline, not a slogan

"You Ain't Gonna Need It" is usually quoted at the feature level, but its real power is architectural. Before adding a system, ask: what user-visible problem does this solve today? If the honest answer is "we might need it later", that later is where it belongs. The cost of adding infrastructure early is not the setup — it is the default path you commit the team to. Every team member now assumes the complex path is the blessed path.

2. The Rule of Three

Do not abstract until the pattern has appeared three times. First occurrence: do it concretely. Second: do it concretely, and note the duplication. Third: now you have evidence of a shape, and you can abstract with data instead of vibes. The Rule of Three turns abstraction from a guess into a response to evidence.

3. Complexity budgeting

Treat complexity as a finite budget, the way you treat a database budget. Every new box must pay for its admission by removing more complexity than it adds. This forces the honest conversation: this cache removes a DB round-trip (good) but adds invalidation, staleness and a second source of truth (bad) — net? If the answer is not clearly positive, the box does not get in. Document the budget in your ADRs (Architecture Decision Records) so a future engineer can see why the system is simple instead of assuming it was an accident.

4. The "can it be a boring choice?" test

When a decision is on the table, ask whether the boring choice — one more box in the monolith, one more SQL query, one more standard HTTP call — works. Boring choices have well-known failure modes, big communities, and no one will ever write a talk about them. That is a feature. You want your exotic risk concentrated in the few places where it is actually necessary, not spread across the whole system for the sake of the diagram.

The whole framework in one ASCII picture:

           SIMPLE SYSTEM                           OVER-ENGINEERED SYSTEM
   ┌───────────────────────────┐          ┌───────────────────────────────┐
   │   app  →  db              │          │  client → gateway → service-a │
   │   one codebase            │          │  service-a → service-b (bus)  │
   │   one deploy              │          │  service-b → service-c → db   │
   │   one way to fail         │          │  three schemas, two queues,   │
   │   boring, fast, debuggable│          │  one event bus nobody owns    │
   └───────────────────────────┘          └───────────────────────────────┘
   failure modes: 2                       failure modes: 7+
   cognitive load: low                    cognitive load: high
   changes: touch one box                 changes: touch four PRs + release
   latency: one hop                       latency: three hops + retries

Chapter 4 — Monolith vs microservices vs modular monolith

The three styles are not a religion; they are a spectrum with different failure profiles. The table is the honest version:

MonolithModular monolithMicroservices
LatencyLowestLowHigh (network hops)
DeployabilityOne unitOne unit, clear seamsIndependent
Team scale-upHardMediumBest
Operational loadLowestLowHighest
Refactor riskHigh (touching everything)Low (within a module)Low (across is hard)
Distributed complexityNoneNoneFull
Right whenSmall team, single domainGrowing team, clear domainsBig org, hard scaling, many teams

The modern consensus — from Thoughtworks' "monolith revivalists" to the rise of modular-monolith frameworks like Spring Modulith — is that the modular monolith is the best default, not because it is fashionable, but because it captures most of the agility with none of the distributed cost. You can always decompose a modular monolith later; the seams are already there. Splitting a distributed monolith is archaeology.

A rule of thumb I actually trust: if you have fewer than five teams, a microservices architecture is almost certainly a mistake in disguise.

Conclusion — elegance is the removal of the unnecessary

The "perfect" architecture is not the one with the most boxes. It is the one where every remaining box has survived an interrogation, where the complexity that exists is essential — demanded by the problem — and none of it is accidental, sneaked in by fashion, fear, or a pretty diagram. Antoine de Saint-Exupéry said perfection is achieved "not when there is nothing more to add, but when there is nothing left to take away." Architecture works the same way.

That is why simple is hard: it is a continual act of removal, and removal is socially expensive. It is easier to agree to add than to agree to cut, because cutting a box questions the cleverness of whoever drew it. Do it anyway. Your on-call engineer, your latency budget, and your future self will thank you.

For practical follow-ups: the same discipline that keeps a Cloudflare-backed blog deployment boring applies to the agentic tools that now touch our systems — keep the architecture simple, and keep the humans in the loop.

First published on hejes.my. If you have an opinion about monoliths, the comment section is open.

// author

Gaara

Chief Operator

Gaara is the human operator behind hejes.my. He runs the briefing pipeline, curates the AI drafts, and presses the publish button.

// join the feed

one fresh insight per week. no spam, ever.