Scalability in business software is usually discussed as though the problem were traffic — thousands of concurrent users, requests per second, load balancing. For an internal system, a portal or a CRM, that is almost never what happens.
What happens is that the application gets slower over three years while the user count barely changes. A screen that opened instantly takes eight seconds. A report that ran in a minute times out. Nothing was deployed; the data simply grew.
That is the actual scaling problem for this class of software, and recognising it changes what is worth doing about it — because the architectural answers usually proposed do not address it at all.
Why data volume rather than user count
A business application accumulates. Orders, calls, records, log entries, audit history — each row is added and few are ever removed. User count grows with headcount, which is slow. Data grows with activity, which compounds.
The consequence is that queries written against a small table behave completely differently against a large one. A query with no supporting index examines every row, and the time that takes is proportional to the row count. At ten thousand rows nobody notices. At two million, the same code is unusable, without a line of it having changed.
What actually helps
Almost all of it is at the database layer, and almost none of it requires changing the application's architecture.
- 1
Index what is queried
The single highest-return action available, by a wide margin. Most slow business applications have a small number of unindexed queries, and adding the indexes converts seconds into milliseconds without touching application code.
- 2
Find the queries rather than guessing
Databases can report their slowest queries. That list is short, specific, and almost always surprising — the slow query is rarely the one people assume.
- 3
Fix the repeated-query problem
Loading a list and then issuing one further query per row is the most common performance defect in application code. It is invisible in development against small data and quadratic in production.
- 4
Paginate everything
Any screen that loads all records will eventually load too many. Pagination should exist before it is needed, because adding it later means changing every consumer of that screen.
- 5
Move heavy work out of the request
Reports, exports and bulk operations belong in a background job. A user waiting on a request while a report generates is a timeout waiting to happen, and the timeout usually arrives at month end.
- 6
Archive what is no longer queried
Data from six years ago that nothing reads still slows every query that scans the table. Moving it to an archive is frequently the simplest large improvement available.
- 7
Cache what is expensive and rarely changes
But deliberately, with a stated expiry. Cache added to hide a slow query leaves the slow query in place and adds a source of stale data.
The order matters. Indexing and finding the actual slow queries resolve the majority of real cases in most business applications, and both are cheap. Everything below them in that list is worth doing and addresses less.
Microservices are the wrong first answer
When a business application is slow, splitting it into services is frequently proposed. For this class of application it is almost always the wrong move, and it is worth being direct about why.
- It does not address the cause. If the constraint is an unindexed query against a large table, that query is equally slow inside a separate service.
- It converts function calls into network calls, which are slower and can fail. Reliability usually decreases.
- It converts a single database transaction into coordination across services, which is a materially harder problem than the one being solved.
- It multiplies operational overhead — deployment, monitoring, and debugging across process boundaries — at a scale where that overhead has no offsetting benefit.
Splitting a system is justified by organisational scale — several teams needing to deploy independently — and by genuinely divergent scaling characteristics between components. Neither typically applies to a business application serving one company.
A well-structured single application with clear internal boundaries, a properly indexed database and background jobs for heavy work will serve a growing business for years, and it can be split later if a real reason appears. The reverse — recombining prematurely split services — is far harder.
Designing so that scaling stays possible
A small number of decisions taken early keep the cheap options available later. None of them costs anything at the start.
| Decision | What it prevents |
|---|---|
| Keep session state out of the application process | Being unable to run a second instance |
| Store uploaded files outside the application server | Files existing on only one machine |
| Make background jobs safe to run twice | Recovery from any failure requiring manual reconciliation |
| Do not assume single-instance execution | Duplicate scheduled work when a second instance is added |
| Separate read-heavy reporting from transactional work | Reports degrading the system users depend on |
| Instrument query time from the beginning | Discovering the slow query only after users complain |
The first four together determine whether the application can run as more than one instance, which is the difference between scaling being a configuration change and scaling being a project.
Knowing where the limit is before reaching it
The difference between a business that scales calmly and one that scales in crisis is whether anyone knew the constraint was approaching.
- Track query time at the 95th percentile, not the average. The average stays acceptable while a minority of users have an unusable experience.
- Track table growth rates, so the threshold can be projected rather than encountered.
- Load-test with realistic data volumes rather than realistic user counts. Testing with a thousand rows validates nothing about behaviour at a million.
- Watch background job queue depth. A queue growing steadily means work is arriving faster than it is processed, and it will fail eventually.
The third point is the one most often missed in testing. Development and staging environments typically hold a fraction of production data, so the exact defect that causes production slowness cannot appear there. Testing against a realistic data volume is what makes performance problems findable before users find them.
Why does a business application get slower over time without any change?
Because data volume grows while the code stays the same. A query without a supporting index examines every row, and the time that takes is proportional to the row count. At ten thousand rows nobody notices; at two million the same code is unusable. User count grows with headcount and is slow, while data grows with activity and compounds — which is why data volume rather than concurrency is the usual constraint for this class of software.
What is the single most effective way to make a business application faster?
Adding indexes to the columns that are actually queried. Most slow business applications have a small number of unindexed queries, and adding the right indexes converts seconds into milliseconds without touching application code. The prerequisite is finding them rather than guessing — databases can report their slowest queries, and that list is usually short and almost always surprising.
Should a slow application be split into microservices?
Almost never as a first response. Splitting does not address the usual cause — an unindexed query is equally slow inside a separate service — while converting function calls into network calls that are slower and can fail, turning single database transactions into cross-service coordination, and multiplying deployment and debugging overhead. Splitting is justified by organisational scale or genuinely divergent scaling characteristics, neither of which typically applies to an application serving one company.
What is the most common performance defect in application code?
Loading a list of records and then issuing one further query for each row. It is invisible during development against small datasets and becomes quadratic in production. It is also usually straightforward to fix once identified, which is why instrumenting query counts per request pays for itself.
How should performance be tested before launch?
With realistic data volumes rather than realistic user counts. Development and staging environments typically hold a fraction of production data, so the specific defect that will cause production slowness cannot appear there. Query time should also be tracked at the 95th percentile rather than as an average, because an average stays acceptable while a minority of users experience the system as broken.
Sources and further reading
- How a custom CRM actually gets built— the design phase where these decisions are taken
- Choosing a technology stack for business software— why the database is the least reversible choice
- Client portals vs customer portals— tenancy, which changes the data model these constraints apply to
- Data retention and archiving for growing businesses— archiving old data, which is often the simplest large improvement
- Infrastructure monitoring that works— percentiles, and knowing the limit before reaching it
- High availability for business systems— what running more than one instance requires beyond the application
- PostgreSQL: indexes— index types and when each applies
- MySQL: optimisation and indexes— the equivalent reference for MySQL, including how the optimiser chooses an index
- The Twelve-Factor App— stateless processes and backing services, which the options table depends on
Services This Relates To
Written by KYCONNECTS Engineering. Client names are withheld under confidentiality.