Scalability is discussed as though it were an architectural style you select at the beginning. In our experience it is closer to a set of habits, and the majority of production incidents in small and mid-sized systems trace back to the same short list of causes.
What actually breaks
- A query that was fine on ten thousand rows and is not fine on two million.
- An N+1 query pattern introduced by an ORM convenience.
- A synchronous call to a third-party API inside a request the user is waiting on.
- A background job with no retry, no dead-letter queue and no alert when it stops.
- A single database connection pool exhausted by one slow endpoint, taking everything else with it.
- Unbounded growth in a table nobody was watching.
None of these are solved by microservices. Several of them are made harder by microservices, because the failure now crosses a network boundary and the trace is more difficult to follow.
The default we start from
For most businesses, the correct starting architecture is unglamorous: one well-structured application, one relational database, a cache, a queue for anything slow or external, and thorough observability. Deployed on managed infrastructure. Boring in every dimension.
This handles far more load than people expect. PostgreSQL on a reasonably sized instance will serve a substantial business without complaint, provided the indexes are right and the queries are not pathological. The engineering effort that would have gone into distributed systems goes into correctness and instrumentation instead.
Choose the architecture that matches your current load and your next order of magnitude. Not the one that matches the company you hope to become in five years.
Habits that carry the weight
- Look at the query plan for anything that touches a growing table.
- Put a timeout on every external call, and a circuit breaker on the ones you cannot control.
- Move anything slow, external or retryable out of the request path and into a queue.
- Set alerts on saturation — pool usage, queue depth, disk — not only on errors.
- Load test before the event you already know is coming, not after it.
- Keep a written record of why each significant decision was made.
When distribution is the right answer
There are real reasons to split a system: independent scaling of a genuinely different workload, isolation of a compliance boundary, or team sizes that make a single codebase a coordination bottleneck. All of those are legitimate. What they have in common is that the pressure is observable before the split, not predicted.
Split when the pain is measurable. Until then, the boring version will outperform the sophisticated one, mostly because it is small enough to understand.