Scale the Bottleneck You Have, Not the Architecture You Admire

team-meeting

Post Details

David Wilson - 04 June 2026

Scale Is a Diagnosis

Every scaling failure begins with a tempting lie: we just need a bigger architecture.

Response times spike, logs fill with timeouts, and the team starts reaching for familiar remedies: add Redis, introduce Kafka, split the monolith, shard the database, move to Kubernetes, or copy whatever a much larger company wrote about in an engineering blog.

Sometimes one of those moves is right. Often it is an expensive step in resume-driven development all too common these days.

Scalability is not one problem. It is a diagnosis.

Before changing the architecture, you need to name the pressure building inside the system. Are reads overwhelming the database? Are writes blocked by contention? Is one customer, post, product, or tenant creating a hot spot? Is the dataset too large for one machine? Is latency being caused by a third-party API? Or has the engineering team grown large enough that deployment coordination is now the bottleneck?

The answer matters because every scaling pattern solves one class of pain while creating another.

A popular historical example, also discussed by Martin Kleppmann in Designing Data-Intensive Applications, is Twitter’s home timeline. The problem was not simply “lots of tweets.” The real pressure came from the relationship between posting a tweet, reading a home timeline, and fanning that tweet out to followers. Kleppmann describes the distribution of followers per user as a key load parameter because it determines the fanout load. [1]

A 2013 High Scalability summary of Twitter’s timeline architecture reported roughly 300,000 timeline-query requests per second, while write traffic was closer to 6,000 requests per second. That asymmetry matters. If the system optimized only for tweet ingestion, it would miss the much larger pressure: delivering timelines quickly at massive read volume.[2]

Twitter’s answer was not to choose one universal pattern. For most users, it made sense to do the heavy lifting at write time: when someone posted, Twitter could fan that tweet out into followers’ home timeline caches so reads stayed fast. But for very high-follower accounts, that write-time fanout became too expensive. A single post from a celebrity could require millions of timeline updates. In those cases, the better trade-off was to fetch and merge those posts at read time instead.

That hybrid approach is the point. Twitter adapted the architecture based on where the bottleneck appeared. It did not worship one pattern. It matched the pattern to the pressure.

The first scaling question should therefore be boring and specific: what exactly is getting worse as usage grows?

Name the Scaling Dimension Under Pressure

Most scaling problems show up along a few recurring dimensions: read throughput, write throughput, fanout, data volume, latency, and team coordination. The mistake is trying to solve all of them at once.

A cache, a queue, a shard key, a larger server, and a new service boundary each target a different kind of pressure. Choosing the wrong one can make the system more complex without making it meaningfully faster, safer, or easier to change.

A useful first pass looks like this:

SymptomScaling dimensionWhat to measure first
Pages are slow under trafficRead throughput / latencyp95/p99 latency, database query time, cache hit ratio
Writes time outWrite throughput / contentiontransaction time, lock waits, queue depth
One tenant, user, or object causes incidentsHot spot / fanoutper-key traffic, per-tenant usage, fanout size
Database growth becomes painfulData volumetable size, index size, working-set size
Releases become slow and riskyTeam coordinationdeploy frequency, lead time, change failure rate, lock-step deployment occurrence

This is where many teams overreact. They see future scale coming and try to conquer every dimension at once. They introduce microservices before they understand domain boundaries. They add queues before they understand failure handling. They shard data before they know the access patterns. They build for an imaginary future load and pay the complexity cost immediately.

That is the real danger of premature optimization: not that optimization is bad, but that optimizing the wrong constraint makes the system harder to change.

Start With the Simplest Scaling Move

Before changing the architecture, start with the simplest scaling move: compute.

Sometimes the system really does need more capacity. Scaling vertically means giving a machine more CPU, memory, disk I/O, or network capacity.

Scaling horizontally means adding more machines behind a load balancer, so the workload is spread across instances.

That is often the right first move because it is simple, reversible, and easy to understand. If the application servers are CPU-bound, adding more instances may buy time. If memory pressure is causing crashes, a larger instance may stabilise the system while you investigate.

But compute only helps when compute is the constraint.

A larger server will not fix a bad query plan. More application instances will not fix a hot database partition. A bigger Kubernetes cluster will not fix stale cache invalidation. And no amount of horizontal scaling will fix a team that cannot safely deploy.

Compute buys capacity. Architecture buys options.

Architecture Should Preserve Options

Architecture is related to scalability because it determines how expensive change becomes.

A good architecture does not need to solve every future scaling problem on day one. It needs to preserve enough optionality that, when the real bottleneck appears, the team can respond without tearing the system apart.

That is architectural reversibility: building the simplest system that handles the current load while keeping future changes possible.

For many products, that means starting with a modular monolith rather than a distributed system.

A monolith is not automatically a mess. A disciplined modular monolith keeps business capabilities separated inside one deployable application. Billing, identity, notifications, reporting, and inventory can have clear internal boundaries without becoming separate network services on day one. This gives the team fast local development, simple transactions, easier debugging, and fewer operational moving parts.

Martin Fowler’s “Monolith First” argument is useful here: many successful microservice systems began as monoliths that were later split, while starting with microservices too early can impose a “microservice premium” before the team understands stable service boundaries.[3]

The goal is not to avoid microservices forever. The goal is to avoid paying the distributed-systems tax before you know what you are buying with it.

Team Scale Is Still Scale

Not every scaling bottleneck is CPU, memory, or database I/O. Sometimes the system can handle the traffic, but the team can no longer safely change it.

Microservices are not magic scalability dust. They are deployment and ownership boundaries. They help when different parts of the system need independent release cycles, independent teams, or independent resource scaling. But they also turn simple function calls into network calls. That means latency, retries, partial failures, versioning, observability overhead, and data consistency problems.

Team scale matters because architecture and communication are connected. Conway’s Law states that organizations tend to produce system designs that mirror their communication structures. As a team grows, architecture often has to reflect ownership boundaries, not just runtime load.[4]

So, when should you split the monolith?

Split it along business boundaries first.

Domain-Driven Design calls these boundaries bounded contexts: explicit boundaries around models such as Billing, Catalog, Fulfilment, or Identity. Fowler describes bounded context as a central DDD pattern for dealing with large models and teams.[5]

That distinction matters because code decomposition and data partitioning are not the same decision.

Splitting Billing into its own module or service may reduce team coordination. Splitting the Billing database changes the physics of the system.

Data Scale: Split State Last

Data is harder to split than code.

You can move code behind a new interface, deploy it separately, or refactor it again later. Moving state is more dangerous. Once data is distributed across machines, queries, transactions, consistency, backups, migrations, and operational tooling all become harder.

That is why sharding should be treated with caution.

Sharding divides a data store into horizontal partitions so different subsets of data live on different machines. It can improve scalability when one database can no longer handle the volume or access pattern. But the shard key is one of the most consequential choices in the system. Microsoft’s sharding guidance notes that sharding physically organises data and that applications must route reads and writes to the correct shard.[6]

A poor shard key can create hot partitions, uneven load, expensive cross-shard queries, and painful future migrations. Sharding should rarely be the first move.

Exhaust simpler options first: better indexes, query-plan analysis, pagination, read replicas, archival, batching, and caching.

Read Pressure: Cache Carefully

Caching is often the fastest way to relieve read pressure, but it is not free.

A cache is a second copy of state. Once data exists in both the database and the cache, the system must decide how stale the cached version is allowed to be, how entries expire, and what happens when invalidation fails.

The cache-aside pattern is common, the application checks the cache first, loads from the database on a miss, then stores the result in the cache for future reads. Microsoft’s cache-aside guidance describes this pattern as a way to load data on demand into a cache while improving performance and helping maintain consistency between cached data and the underlying store.[7]

But “helping maintain consistency” is not the same as guaranteeing it. If a user updates their profile and the cache invalidation fails, the system may continue serving stale data.

That does not mean caching is bad. It means caching should be aimed at the right pressure. Use it when data is read frequently, expensive to compute or fetch, and tolerant of some staleness. Do not use it to hide unknown database problems. If the cache hit ratio is poor, or the data must always be perfectly fresh, the cache may add needless complexity without solving the core issue.

Write Pressure: Move Work Out of the Request Path

For write-heavy systems, the more useful pivot is often asynchronous processing.

In a synchronous request, the user waits while the server validates input, writes to the database, calls third-party services, sends email, updates search indexes, emits analytics, and returns a response. Under load, that model burns request threads and couples user latency to every downstream dependency.

A queue breaks that chain.

The server accepts the request, stores the durable intent, and lets workers process slower tasks outside the user’s critical path. Azure’s queue-based load-levelling pattern describes this as using a queue as a buffer between a task and a service so intermittent heavy loads can be smoothed out.[8]

Queues also require idempotency. Messages can be delivered more than once, and workers must tolerate duplicates. AWS SQS documentation explicitly advises designing applications so processing the same message more than once does not cause adverse effects.[9]

Async processing can protect the request path, but it introduces its own responsibilities: retries, dead-letter queues, ordering concerns, duplicate delivery, consumer lag, and operational visibility.

Measurement Closes the Loop

Observability is not a separate scaling dimension. It is the feedback loop that tells you which dimension is failing.

You cannot scale what you cannot see. Every architecture change is a hypothesis: “this cache will reduce read latency,” “this queue will protect the API under spikes,” “this shard key will distribute load,” “this service extraction will improve deployment speed.”

Hypotheses need measurement.

At minimum, track latency, traffic, errors, saturation, cache hit ratio, database query time, queue age, retry count, and consumer lag. Google’s SRE guidance identifies latency, traffic, errors, and saturation as the four golden signals of monitoring for user-facing systems.[10]

For user-facing systems, define Service Level Indicators and Service Level Objectives so reliability work is prioritised against user-visible outcomes, not gut feel.

Once the system spans services, caches, queues, and databases, logs and CPU graphs are not enough. You need traces. OpenTelemetry describes traces as showing the path a request takes through an application, whether that application is a monolith or a mesh of services.[11]

Measurement turns scaling from panic into engineering. It tells you whether the bottleneck moved, whether the change helped, and whether the new complexity was worth it.

Conclusion

The rule is simple: scale the bottleneck you have, not the architecture you admire.

Use compute scaling while compute is the constraint. Use a modular monolith while coordination is cheap. Add caching when reads dominate. Add queues when synchronous work blocks the request path. Partition data only when one data store can no longer handle the load. Extract services when team ownership or isolated resource needs justify the distributed-systems tax.

Scalability is not a badge of honour. It is an operational constraint.

Name the pressure. Choose the smallest reversible response. Measure whether it worked. Then change the architecture only when the evidence says the current one has become the bottleneck.

References

Let's collaborate

Tidon Tech Logo

Interested In Working With Us?

Start a conversation now.

team meeting image
Let's unlock your business potential.