top of page

Architecture Decisions You'll Regret

  • Shawn West
  • May 8
  • 11 min read

Updated: Aug 9

Five patterns that look like senior engineering on the whiteboard and turn into two years of interest payments. The reason they're hard to catch isn't ignorance — it's that the bill arrives long after the decision feels finished. Here's the mechanism behind each one, and the test that tells you whether you're about to make it.

Every architecture decision looks smart the day you make it. You have a whiteboard, a set of requirements, and a team that nods. The regret is invisible at decision time because the cost is deferred — it accrues quietly, one feature at a time, and by the time it's obvious the whiteboard is long gone and the decision is load-bearing. That delay is the whole problem. It means you can't feel a bad architecture decision the way you feel a bad line of code; you feel it a year later, as a tax on everything.

Look closely at the five most common regrets and they share one root. None of them is a technology failure. Each is a decision to commit to a structure — a service boundary, a scaling strategy, an abstraction, a build, a schema — before the team had discovered enough about the domain to place it. That's not a coding mistake. It's a discovery mistake wearing an architecture costume, and it's the thread running through all five.

You can't draw a boundary you haven't discovered yet

(Developed example — anonymized composite.)

Picture the kickoff. Four engineers, a whiteboard, a rectangle for each noun in the requirements doc: User, Order, Notification, Payment. Someone connects the boxes, and by lunch there are four services, four repos, four databases, and a diagram that looks exactly like the ones in the conference talks. It reads as senior engineering. It photographs well.

Here's the moment it turned, twelve months in. A product manager asked for something trivial: when an order ships, email the customer a receipt showing their loyalty balance. On the whiteboard that's one arrow. In the running system it was Order calling Notification calling User calling Payment — four services, four deploy pipelines, four on-call owners — to render one email. The change that would have been a fifteen-line function now needed a versioned API contract and a coordinated release in a specific order, because Notification read a field Order hadn't shipped yet. The team was spending more engineering time on service-to-service plumbing than on the product.

That's the tell, and it has a name: the deploy-together signal. If you can't ship a routine feature without releasing two or more services in lockstep, you don't have microservices — you have a distributed monolith: all the operational cost of services, none of the independence. It pays network latency for what used to be function calls, and answers every debugging question with four stack traces instead of one.

The mechanism is simple once you see it. The boxes on the whiteboard were nouns from the requirements, not seams in the domain. A real service boundary sits where two parts of the system change at different rates for different reasons — where Payment evolves on the compliance team's calendar while Catalog moves on merchandising's. Those seams are almost impossible to see from a requirements doc. They only become visible after the system runs under real usage and you watch which changes travel together and which stay put. Drawing the lines on day one isn't rigor; it's guessing with extra infrastructure, and then living inside the guess.

The diagnostic (run it on your own repo): tag your last twenty merged pull requests with the services each one touched. Mostly one service per change → your seams are real. Mostly two-or-more → you've built a distributed monolith, and the fix isn't more tooling, it's collapsing the chatty services back together until the true boundary reveals itself.

When the default is wrong (§ the exception): separate services are justified on day one when a boundary is imposed from outside the code — a hard regulatory data-residency line, two teams that genuinely ship on independent cadences, or a component with sharply different scaling needs (a public webhook ingester in front of an internal batch system). The test is whether you can name the external force holding the boundary apart. "It felt cleaner" is not an external force.

The boundary isn't a decision you make at the whiteboard. It's a measurement you take after the system tells you where it wants to split.

Building for load you don't have — and can't yet name

"We need to handle a million concurrent users." You need to handle the twelve people in your beta. Premature optimization at the architecture level isn't loop-unrolling; it's making the entire system more complex to survive a load that doesn't exist and may never.

A team stands up an event-driven pipeline on Kafka because "we might process high-throughput streams later." Weeks go into topics, partitions, consumer groups, and dead-letter queues. The system processes on the order of dozens of events per minute — a single Postgres table and a polling query would have done it in an afternoon. Another team runs multi-region active-active because "we need to be globally available," for a user base sitting in one time zone; the conflict-resolution complexity ties up an engineer for a quarter to improve latency for users who don't exist.

The carrying cost is the part teams underweight. It isn't the week you spend building it — it's the cognitive tax on every change afterward. Every new hire now has to understand the event bus, the partition strategy, the replication and conflict model before they can safely ship a feature, on a system whose actual traffic a cron job could serve. Complexity you add for an imagined future is paid, in full, in the present, by everyone who touches the code.

The diagnostic: before you add the scaling machinery, name the metric and the threshold that would justify it — "we build the stream pipeline when sustained ingest passes N events/second for M minutes." If you can't name the number, you're not scaling on evidence, you're scaling on imagination. If you can name it, you've just written the alert that will tell you when it's actually time — so add the alert now and the machinery later.

When the default is wrong: you legitimately build ahead of load when the load is contractually or physically known in advance — a launch you've pre-sold, a regulated volume, a migration with a fixed cutover date. Known future load is evidence. "We might get big" is not.

The abstraction that abstracted away your best tools

Somewhere in every project someone says, "let's add an abstraction layer so we can swap this out later." It sounds prudent. It usually isn't.

A team wraps all database access in a house-built layer so they can "switch databases if needed." The wrapper, to stay generic, forbids the database-specific features that would make their queries simpler and faster — so they spend two years working around the limits of their own abstraction, and never switch databases. Another team builds a "cloud-agnostic" layer to "move between AWS and Azure someday." It can only express the lowest common denominator of both, which locks them out of the managed services — queues, functions, managed auth — that would have deleted entire categories of code. They never switch clouds. They pay the abstraction tax on every feature instead.

The mechanism: a speculative abstraction has to serve a future that hasn't arrived, so it's designed against an imagined interface rather than a real one. It obscures what the code actually does, forbids the concrete tools you already pay for, and hedges a risk — provider switching — that almost never materializes. The wrong abstraction is worse than none, because no abstraction at least leaves the concrete tool in reach.

The diagnostic: ask how many times, in this system's life, you have actually swapped the thing the abstraction protects. Usually zero. Then ask what that abstraction has cost you this quarter in features you couldn't build the direct way. If "times swapped" is zero and "quarterly tax" is nonzero, the abstraction is a liability. The alternative isn't recklessness — it's a thin, clean interface to the concrete thing you're actually using. If the swap day ever comes, a clean seam makes it manageable; the speculative layer just makes every day until then worse.

When the default is wrong: abstract early when you have two real implementations today (you genuinely run on two clouds now), or when the concrete dependency is known-volatile (an unstable vendor API you've already been burned by). Two real cases beat one imagined one.

The custom build for a problem the industry already solved

Authentication. Job scheduling. Full-text search. Email delivery. Rate limiting. Each is a solved problem with mature, battle-tested implementations — and each gets rebuilt from scratch by a team convinced its requirements are special. They almost never are.

A team builds custom auth because "multi-tenant, the existing options don't fit." Three months and two security incidents later they migrate to a managed provider — which supported the multi-tenant case all along; they hadn't read the docs closely. Another writes a custom scheduler for "exactly-once execution," then discovers exactly-once in a distributed system is one of the genuinely hard problems in computing; what they ship is at-least-once — precisely what every off-the-shelf scheduler already gives you, minus a decade of hardening.

The mechanism is a predictable pair of misjudgments: optimism about how hard the problem is, and underestimation of how mature the existing solution is. Both point the same direction — toward a build that looks like a month and becomes a permanent maintenance line, including the security surface you now own forever.

The diagnostic — the articulation test: you may build custom only when you can state, with a documented specific, why the mature option cannot work. "It doesn't do exactly what we want" fails the test. "It cannot support data residency in these three jurisdictions, and here is the paragraph in its docs that confirms the limit" passes. If you can't cite the blocking line, you haven't finished evaluating — you've finished wanting to build.

When the default is wrong: build custom when the capability is your product's differentiator (a search company builds its own ranking), or when a documented, verified requirement genuinely has no market answer. Owning code you don't have to is a cost; own it only where it earns its keep.

The schema designed for flexibility you'll rarely use

The impulse is reasonable: "we don't know exactly what data we'll need, so let's keep the schema flexible." It shows up as entity-attribute-value tables — every fact stored as a generic key-value row — or as "just dump it in a JSON column" because defining columns felt too committal. Both trade query integrity for a flexibility you seldom actually exercise.

Make it concrete. Find every order over one hundred dollars. In a typed schema that's a single indexed comparison. In an EAV table it's a join, a cast, and a prayer that every row stored the amount as a number and not a string with a dollar sign — and you've lost database-level validation, meaningful indexes without contortions, and the self-documenting quality of a schema that tells the next engineer what the system actually manages. The JSON-column version is milder but fails the same way: the moment you need to filter, sort, or guarantee a shape, the flexibility you bought becomes the wall you query around.

The mechanism: a schema isn't just storage, it's the contract that makes queries simple and data trustworthy. Dissolve the contract for flexibility and you move the cost from "define columns once, up front" to "reconstruct the missing structure in every query, forever."

The diagnostic: name the three questions this data must answer in the next quarter — the three real queries. If you can name them, you know enough to type the schema, so type it. If you can't name a single concrete query, you're not ready to build the data layer at all; a week understanding the domain beats a year querying around a shape that fights you.

When the default is wrong: genuinely sparse, open-ended attributes (user-defined custom fields, heterogeneous third-party payloads) are a real fit for a typed core table plus a well-indexed JSONB column for the long tail — a deliberate, bounded exception, not a way to avoid deciding.

How to tell, before you commit

The five regrets are one mistake in five costumes: committing to structure ahead of discovery. The antidote isn't less design — it's design gated on evidence. Three questions, each with an observable test, before any structural decision:

  1. What problem am I solving right now? Not next quarter, not at scale. If the honest answer starts with "eventually we might need to…", stop — that's imagination, not a requirement. Build for the load, the boundary, and the queries you can actually name today.

  2. What does it cost to be wrong — and how reversible is it? A UI choice you can revert in a week invites experimentation. A data model, a service topology, or a vendor lock-in that takes six months to unwind demands you validate the assumption first. Match the rigor of the analysis to the cost of the reversal (this is the decision-type lens — reversible choices and one-way doors are not the same decision).

  3. Am I copying someone else's scale? The famous architectures solve famous-scale problems. Microservices answered a thousand-engineer coordination problem; a custom datastore answered millions-of-transactions-per-second. Borrow the reasoning behind a pattern, never the pattern itself — the shape that solves their problem at their scale will not solve yours at yours.

The tradeoff you're actually making

You add now…

To buy…

You pay…

Worth it when…

Warning sign

Service split

Independent deploy/scale

Network calls, distributed debugging, coordination

An external force holds the boundary apart

Features touch 2+ services

Scaling machinery

Headroom for future load

Cognitive tax on every change

The load is contractually known

You can't name the trigger metric

Abstraction layer

Freedom to swap later

Loss of the concrete tools you pay for

You run two real implementations today

Times actually swapped: zero

Custom build

A perfect fit

Permanent maintenance + security surface

It's your differentiator, or docs prove no fit

You can't cite the blocking limitation

Flexible schema

Room for unknown data

Query complexity, lost integrity

Attributes are genuinely sparse/open-ended

You can't name three real queries

Every row is the same shape: complexity is certain and paid now; the benefit is speculative and paid maybe-never.

A method you can run: the discovery-gated decision record

Make the discovery gate a ritual, not a vibe. For any decision that's expensive to reverse (Q2 above), write a short decision record before you commit, and require it to answer four things: the problem as it exists today (with the metric, boundary, or queries named), the reversal cost, the evidence you actually have versus the future you're imagining, and the trigger — the observable signal that would tell you it's time to add the complexity you're deferring. If the record can't name today's evidence and tomorrow's trigger, the decision isn't ready; it's a guess you're about to make load-bearing. Pair it with the twenty-merge audit above as a recurring check on the boundaries you've already drawn.

What to do next

Pick your riskiest current architecture assumption — the boundary, the scaling bet, the abstraction, the build, or the schema you'd most hate to unwind — and run its diagnostic from this piece this week. Twenty merges tagged by service. The trigger metric named or admitted-missing. The blocking limitation cited or the build reconsidered. One honest test on the one decision you can least afford to be wrong about.

The lasting point: the architecture you regret is the one that felt like good engineering — the clever abstraction, the early decomposition, the optimization for load you didn't have. It felt smart because it was engineering in service of imagination. The architecture you don't regret is usually boring: proven tools, solving today's named problem, simple enough that a new hire understands it in a week, with clean seams that leave room to change when the evidence — not the whiteboard — tells you to.

Related on ShiftQuality: the build-versus-buy call in depth (Build, Buy, or Borrow); how to weight reversibility and evidence (Technical Decision-Making Under Uncertainty); instrumenting a system so it tells you when to change (Observability as a System Property); and scale-appropriate choices for small teams (Technology Decisions for Small Businesses).

bottom of page