BlogMay 28, 2026

System Design for Startups: Decisions That Scale vs Decisions That Kill

Hamza Ali
System Design for Startups: Decisions That Scale vs Decisions That Kill
The architecture decisions that seem smart at 100 users become nightmares at 100K.I've designed and scaled 25+ systems from MVP to production. Some of those decisions aged like fine wine. Others aged like milk left on a dashboard in July.The difference was never about using the "right" technology. It was about knowing which decisions are reversible and which ones cement you into a path you can't escape.Every technical decision falls into one of two buckets:
- Type 1: One-way doors. These are hard or impossible to reverse. Your primary database. Your authentication model. Your multi-tenancy strategy. Your API contract with external consumers. Get these wrong and you're looking at a rewrite, not a refactor.
Type 2: Two-way doors. These are easy to reverse. Your frontend framework. Your CSS approach. Your CI/CD pipeline. Your logging library. Get these wrong and you spend a weekend migrating, not a quarter.Most startup engineers treat everything like a Type 1 decision and over-engineer. Or worse, they treat Type 1 decisions like Type 2 and under-invest.Here's my breakdown of what matters and what doesn't at each stage.At this stage, you don't have a scaling problem. You have a survival problem.Decisions that matter:
  • Pick a boring database. PostgreSQL. Not because it's the best at any one thing, but because it's good enough at everything and you won't need to replace it until you're well past product-market fit. I've seen startups pick MongoDB because "we don't know our schema yet" and then spend months fighting data consistency issues. PostgreSQL with JSONB columns gives you schema flexibility without sacrificing relational integrity.
  • Design your API contract carefully. Your API endpoints are a Type 1 decision the moment another team or client depends on them. Version from day one. Use `/api/v1/` in your paths. Make response shapes consistent. When you need to change a field, add the new one alongside the old one instead of renaming.
  • Choose a monolith. I'm not being controversial for the sake of it. A monolith deployed as a single service is faster to build, easier to debug, simpler to deploy, and cheaper to run than microservices. You can always extract services later. You can't easily merge them back together.
Decisions that don't matter yet:
  • Whether you use REST or GraphQL (pick whichever your team knows)
  • Your hosting provider (you'll migrate before it matters)
  • Your state management library
  • Your testing framework
This is where most systems start showing stress. Not from raw load, but from complexity.The database starts hurting first. Not because PostgreSQL can't handle 10K users (it can handle millions), but because your queries have grown organic and wild. You have N+1 queries buried in your ORM. You have table scans on columns that should be indexed. You have a reporting query that locks a table for 30 seconds.The fix at this stage is never "switch databases." The fix is:
- 1. Add database monitoring. I use pg_stat_statements to find slow queries.
- 2. Add indexes based on actual query patterns, not theoretical ones.
- 3. Separate read-heavy reporting queries onto a read replica.
- 4. Introduce connection pooling if you haven't already.
Authentication decisions start biting. If you rolled your own auth at the MVP stage, this is where it hurts. Password reset flows have edge cases. Session management has security holes. OAuth integration requests start piling up.My strong recommendation: migrate to a managed auth provider (Auth0, Clerk, Supabase Auth) before you hit 10K users. The migration is painful. It's less painful at 10K than at 100K.The monolith needs boundaries. You don't need microservices yet. But you need internal boundaries. Organize your code into modules with clear interfaces. The billing module talks to the user module through a defined interface, not by reaching into its database tables.This is the single most valuable architectural pattern for growing startups: modular monolith. You get the deployment simplicity of a monolith with the organizational clarity of services.Now scaling actually matters. But not everything needs to scale equally.Extract the bottleneck, not everything. In every system I've scaled past 10K users, there's one subsystem that hits the wall first. Usually it's one of: image/file processing, real-time notifications, search, or background job processing.Extract that one thing into a separate service. Leave everything else in the monolith.For one client, search was the bottleneck. We extracted the search functionality into a dedicated Elasticsearch service with its own API. The rest of the application stayed monolithic. That one extraction handled our scaling needs for the next two years.Introduce caching deliberately. Caching is not free. Every cache is a consistency problem. Add caching only where you have measured performance problems, and always define an invalidation strategy before you add the cache.My caching hierarchy: browser cache for static assets, CDN for public pages, application-level cache (Redis) for expensive database queries, and database query cache as a last resort. Each layer adds complexity. Add them in order, and only when needed.Background jobs become critical. Anything that takes more than 200ms should not happen in the request/response cycle. Email sending, PDF generation, webhook delivery, data aggregation: all of these belong in a background job queue.I use BullMQ with Redis for most projects. The key insight: design your jobs to be idempotent from day one. Jobs will fail and retry. If retrying a job causes duplicate charges or duplicate emails, you have a bigger problem than scaling.These are the patterns I've seen cause real damage:
- Premature microservices. A 5-person startup with 12 microservices and a Kubernetes cluster is not engineering excellence. It's engineering theater. The deployment complexity alone will consume 40% of your engineering bandwidth. Ship a monolith. Extract when it hurts.
Wrong database for the access pattern. Using a document database for relational data (or vice versa). Using a graph database because "our data is a graph" when a SQL join table would work fine. Match the database to your actual query patterns, not your data model on a whiteboard.Shared mutable state in distributed systems. The moment two services write to the same database table, you've built a distributed monolith with all the downsides of both architectures. If you must extract services, each service owns its data. Period.Ignoring observability. You can't fix what you can't see. Structured logging, error tracking, and basic APM should be in place from week one. I've debugged production issues at 3 AM. The difference between a 15-minute fix and a 4-hour nightmare is always whether you can see what happened.Building "for scale" without measuring. I once reviewed a system where the team had implemented event sourcing, CQRS, and a custom message bus for an application with 200 active users. They spent 6 months building infrastructure. Their competitor shipped features. Their competitor won.After 25+ systems, the pattern is clear: the best architecture is the one that lets you ship features fastest at your current scale, while keeping the door open to change when you outgrow it.That means: boring technology, clear boundaries, measured decisions, and the discipline to not solve tomorrow's scaling problem today.The startups that win aren't the ones with the most sophisticated architecture. They're the ones that matched their architecture to their actual needs and saved their complexity budget for the problems their users care about.
Share this post:
On this page