Skip to content
Custom Software & CRM7 min read

Building business applications that scale

Most business applications do not fail under load. They fail under data — a query that was instant against ten thousand rows and unusable against two million. Scaling is mostly a database problem, and mostly solvable without any architectural change.

MySQLPostgreSQLRedisLaravelIndexes

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. 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. 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. 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. 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. 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. 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. 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.

Decisions that preserve future options
DecisionWhat it prevents
Keep session state out of the application processBeing unable to run a second instance
Store uploaded files outside the application serverFiles existing on only one machine
Make background jobs safe to run twiceRecovery from any failure requiring manual reconciliation
Do not assume single-instance executionDuplicate scheduled work when a second instance is added
Separate read-heavy reporting from transactional workReports degrading the system users depend on
Instrument query time from the beginningDiscovering 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

Services This Relates To

Written by KYCONNECTS Engineering. Client names are withheld under confidentiality.

Talk Through Your Requirements

We typically respond within 4–8 business hours.