Scaling problems are rarely about traffic. They are about the decisions taken in the first three weeks, when nobody is watching.
When a client says a system does not scale, the conversation usually starts with servers. In our experience the server is almost never the problem. The problem is a set of decisions taken in the first three weeks, when the product had ten users and nobody was watching.
Adding capacity is easy and well understood. What resists scaling is a data model that encoded an assumption which later turned out to be wrong: that an order belongs to exactly one customer, that a document has one author, that a price is a single number. Each of these is reasonable on day one and expensive on day four hundred, because by then the assumption has spread into reports, integrations, and the mental model of everyone using the system.
We spend the discovery stage looking specifically for assumptions that will not survive growth. Not every one of them is worth designing around. The point is to make the choice deliberately rather than discover it during an incident.
Across the systems we have taken over from other teams, the first failures are remarkably similar, and none of them are about raw traffic. A dashboard that recalculates a year of history on every page load is fine with two hundred records and unusable with two hundred thousand. A nightly job that assumed it would always finish before the morning starts overlapping with itself. An export that was written to hold one client's data in memory meets a client ten times the size.
The pattern is that each of these worked correctly at every point during development. They were not bugs. They were sizing assumptions that nobody wrote down, so nobody re-checked them when the size changed. Writing the assumption into the ticket ("this report is expected to cover at most one quarter") costs a sentence and turns a future incident into a planned piece of work.
A frequent instinct is to reach for distributed infrastructure early: message queues, service meshes, several databases. It rarely helps. A single well-designed PostgreSQL instance carries more load than most B2B products will ever see, and it does so with tooling every engineer already understands.
The effort is better spent on the model: clear boundaries between parts of the system, explicit contracts between them, and a schema that can absorb a new requirement without a rewrite. That work is invisible in a demo and decisive two years later.
There is a second reason to keep infrastructure dull. Every additional moving part is something that has to be monitored, upgraded, secured, and understood by whoever is on call at two in the morning. Complexity that buys real headroom is worth it. Complexity adopted because it looked like the professional choice is a tax paid every month.
In most business systems the two halves of the workload diverge early. Writes stay roughly proportional to the number of people doing work: orders placed, tickets logged, documents approved. Reads grow with everyone who wants to look at the result, and they grow much faster once management, customers, and integrations all want their own view of the same data.
Separating the two is usually the highest-value structural change a growing system can make, and it can be done in stages. A read replica removes reporting load from the transactional database. A materialised view turns an expensive aggregate into a cheap lookup. A cached response with an explicit invalidation rule handles the pages everyone opens and nobody edits. None of this requires a new architecture, and all of it buys time.
What does require care is deciding how stale each view is allowed to be. A finance report that lags by five minutes is usually fine. A stock level shown during checkout is usually not. That is a business question, and it belongs in the specification rather than in a developer's judgement call.
Growth adds people as well as records, and a system that only one engineer can safely change has a ceiling regardless of its infrastructure. The practical measures are unglamorous: boundaries in the code that match boundaries in the business, so two people can work without colliding; a test suite fast enough that anyone will actually run it; an environment a new developer can start on the first day rather than the first week.
This matters even more when the team is partly external. The systems we hand over are designed on the assumption that someone we have never met will need to extend them, which tends to produce clearer contracts than designing for the people currently in the room.
On every engagement, the architecture stage produces a written account of what we expect to multiply, where the consistency boundaries sit, and which decisions we consider reversible. Clients get that document alongside the code. When something needs to change later, the reasoning is there, which is usually worth more than the diagram.
We also agree what will be measured from the first release: the queries that carry the product, the jobs that must finish inside a window, and the numbers that would tell us an assumption has expired. Scaling work is far cheaper when it starts from evidence rather than from the moment a client calls to say the system feels slow.