System Design Red Flags: 7 Decisions That Come Back to Haunt You


System design isn’t an event. It’s an ongoing negotiation with complexity, clarity, and change.
And yet under pressure to ship, many teams make subtle, “temporary” design decisions that age like milk. What feels like an MVP shortcut often grows into untestable, unscalable architecture.
In this post, I’m sharing 7 red flags that often seem harmless at the start but become some of the most costly mistakes in long-lived systems. If you’ve ever looked at a piece of infrastructure and thought "why did we do it this way?", this list might explain a few things.
1. Tightly Coupling Services "Just for Now"
“Let’s just call the other service directly we’ll decouple it later.”
Direct service-to-service calls feel like the simplest integration strategy. But over time, those shortcuts turn your architecture into a tightly woven knot. You change one thing and six things break.
Real-world example:
For example, the user service depended directly on the billing service, which in turn depended on the auth service, which depended back on the user service for email lookup. Deployment became a chain reaction. Eventually, they couldn’t deploy any of the core services independently even with feature flags.
Better:
Design services to be loosely coupled and independently deployable. Use queues, event buses, or APIs with graceful fallbacks. If you absolutely must make direct calls, isolate them behind interfaces you can mock or stub.
2. Underestimating the Cost of "Eventually Consistent" Systems
Eventual consistency sounds fine… until it breaks something users care about.
Distributed systems must accept trade-offs. But one of the most misunderstood is eventual consistency. It’s easy to say “users won’t notice the delay” until they absolutely do.
Real-world example:
For example, a “purchase confirmed” event was processed out of order. Users saw “Your item has shipped” before “Your order was placed.” which causes inconsistent behaviour to customers.
Better:
Use eventual consistency deliberately, not by default. Know what consistency guarantees each domain requires. Where strict ordering matters (money, inventory, security), either enforce strong consistency or design UX to acknowledge delays (e.g., "Your order is being finalised").
3. Ignoring Observability Early On
If you don’t measure it, you don’t own it.
Many systems go live with zero instrumentation. Then the first production incident hits. Logs Missing. No Metrics & Traces.
Real-world example:
An e-commerce backend team struggled to reproduce a critical checkout failure reported by users. Logs had already rotated out, and without request IDs or trace context, they couldn’t follow the transaction flow across services. What should have been a two-hour investigation go on to two-week scramble with lost revenue, frustrated customers, and no clear root cause.
Better:
Make observability a first-class design concern:
Add correlation IDs to all logs
Track error rates and latency histograms
Expose service health via
/health
endpointsUse OpenTelemetry or similar to wire traces across services
A system without observability is just hoping for the best.
Thanks for reading System Design Unfolded! Subscribe for free to receive new posts and support my work.
4. Picking a Database Before You Understand the Access Patterns
The database you choose defines what’s easy and what’s painful.
Most teams pick Postgres, Mongo, or DynamoDB based on familiarity or hype without analysing how the data will actually be queried.
Real-world example:
A team used MongoDB for a high-read analytics workload requiring complex joins. Query performance plummeted at 10x traffic. They spent weeks denormalizing data and eventually migrated to BigQuery at significant cost.
Better:
Design queries before schema. Ask:
What are the hot paths?
What are the access patterns?
How do you paginate, index, cache?
Let your system’s shape inform your database not the other way around.
5. Assuming All Communication Must Be Synchronous
Just because it’s easy to call an API doesn’t mean you should.
Synchronous systems feel simple until one slow dependency takes the whole system down.
Real-world example:
An e-commerce platform relied on synchronous REST calls between its cart, inventory, and payment services. When the external payment gateway experienced a 1-second delay, those delays cascaded carts hung, inventory locks piled up, and thread pools filled. CPU usage spiked, response times increased, and customers experience degraded. A single slow dependency brought the entire system to its knees.
Better:
Does this operation really need to block?
Use:
Async queues for non-critical updates
Webhooks or pub/sub for downstream systems
Retry strategies and timeouts to isolate failures
Synchronous calls are fine in moderation. But latency compounds and availability cascades.
Beyond REST: How to Choose Between gRPC, GraphQL, Events, and More
6. Skipping Schema Contracts in Internal APIs
Without contracts, integration becomes telepathy.
Internal teams often skip Protobuf/GraphQL schemas because “we’re all in the same Slack.” But without explicit contracts, small changes introduce big bugs.
Real-world example:
A frontend broke when the backend renamed user_id
to uid
for consistency. The deployment passed CI, but not reality. No versioning, no schema diffing, no warning.
Better:
Always version your internal APIs and publish schemas. Use tools like:
Protobuf with backwards compatibility checks
GraphQL with contract validation (e.g., Apollo Safe Deploys)
APIs are your product even internally.
7. Treating System Design as a One-Time Activity
System design is not an artifact. It’s a process.
Too many teams design once, draw some diagrams, then never revisit them even as scale, requirements, and teams change.
Real-world example:
An e-commerce startup grew from 3 engineers to a team of 40, but never revisited their original system design. The architecture was built for a maximum of 1 million users but as the customer base grew to over 10 million, key assumptions broke down. Product search slowed, order processing lagged, and the database couldn’t handle the load. With no migration plan in place, fixing the issues became a painful, months-long effort.
Better:
Make design reviews part of your operating process:
Revisit architecture docs quarterly
Write ADRs (Architecture Decision Records)
Schedule design retros after major incidents
Design isn’t a phase it’s an ongoing conversation with your codebase.
Final Thoughts: Good Systems Age Well
The best-designed systems aren’t just clever they’re resilient to change. They age gracefully because the teams behind them made conscious trade-offs, avoided short-sighted wins, and revisited decisions over time.
Will this choice still make sense when we double in size? When the team changes? When traffic spikes overnight?
If not, pause. Refactor. Rethink.
If this helped you avoid even one architectural regret, consider subscribing.
I write regularly about backend systems, architecture at scale, and the human side of software engineering.
Subscribe to my newsletter
Read articles from Anil BT directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
