✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.2.1.2 Scaling Simplicity

A focused guide to Scaling Simplicity, connecting core concepts with practical Docker and container operations.

Scaling simplicity is the design goal of making horizontal scaling, running more replicas of the same container to handle more load, a trivial, mechanical operation requiring no special coordination, rather than something that requires careful, manual orchestration each time it happens, and achieving it depends almost entirely on how statelessly the application itself is designed, not on anything specific to Docker's own scaling commands.

Why scaling commands are never the actual bottleneck

Docker and every orchestrator built on top of it make adding replicas mechanically trivial:

docker compose up -d --scale api=5
kubectl scale deployment my-api --replicas=5

The command itself is never the hard part; the actual difficulty lies entirely in whether the application can correctly handle running as five independent, simultaneously active instances at all, which is a property of the application's own design, not of the orchestration tooling issuing the scale command.

Externalizing session state

An application that stores user session data in its own local, in-memory state breaks the moment more than one replica exists, since a user's session created on one replica is invisible to a different replica that happens to handle their next request:

const sessions = new Map(); // in-memory, replica-local session store
app.use(session({
  store: new RedisStore({ client: redisClient }),
}));

Externalizing session state to a shared store reachable by every replica equally, rather than keeping it local to whichever specific replica happened to handle the original request, removes this entire class of scaling obstacle, since any replica can then correctly handle any request regardless of session history.

Avoiding local file system dependencies for shared data

An application that writes uploaded files, generated reports, or any other data directly to its own local filesystem and later expects to read that same data back faces the same fundamental problem once more than one replica exists, since a file written by one replica is invisible to a sibling replica that happens to handle a later request needing that same file:

fs.writeFileSync('/app/uploads/file.pdf', data);
await s3.putObject({ Bucket: 'uploads', Key: 'file.pdf', Body: data });

Moving this kind of shared data to an external object store or a genuinely shared, network-accessible storage system removes the dependency on any specific replica's own local filesystem, which is what actually allows requests to be load-balanced freely across every replica without regard for which one happened to handle some earlier, related request.

Designing for statelessness at the request level

The strongest, simplest form of scalability comes from each individual request being entirely self-contained, requiring nothing from any prior request's specific handling beyond what is retrievable from a shared, external source; this property, statelessness at the request level, is what allows a load balancer to route any given request to any available replica with zero coordination overhead:

app.get('/api/orders/:id', async (req, res) => {
  const order = await db.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
  res.json(order);
});

This handler depends only on the database, a shared resource every replica can reach equally, and nothing about which specific replica processes the request affects the outcome at all, which is exactly the property that makes scaling this particular service trivial.

Background jobs and worker coordination

For services performing background work rather than handling synchronous requests, scaling simplicity depends on the job queue or task distribution mechanism itself correctly preventing the same job from being processed redundantly by more than one worker replica simultaneously:

const job = await queue.dequeue(); // atomic dequeue prevents duplicate processing across workers
await processJob(job);

A queue or task distribution system providing this atomic dequeue guarantee is what allows adding more worker replicas to directly and safely increase processing throughput, without any additional coordination logic needing to be built into the application itself beyond correctly using the queue's own provided guarantees.

Avoiding replica-specific identity dependencies

An application that depends on its own specific, fixed identity, a hardcoded hostname, a specific assumed IP address, or any other characteristic unique to one particular instance rather than shared across every replica equally, similarly breaks scaling simplicity, since each new replica introduced would need some special, individual accommodation rather than being a simple, interchangeable copy:

const instanceId = process.env.HOSTNAME; // unique per replica, fine for logging
const hardcodedIp = '10.0.0.5'; // assumes only one specific instance, breaks with multiple replicas

Using a replica's own, naturally unique identity for something like log correlation is fine and expected; building actual application logic that assumes there is only ever one specific instance, or a fixed, known set of instances, is what undermines scaling simplicity.

Testing scaling behavior directly

Confirming an application genuinely behaves correctly under multiple simultaneous replicas, rather than assuming statelessness based on code review alone, is worth verifying directly through an actual, deliberate test:

docker compose up -d --scale api=3
for i in $(seq 1 20); do curl -s http://localhost/api/session-test; done

Running a sequence of requests against a load-balanced, multi-replica deployment and confirming consistent, correct behavior regardless of which specific replica happened to handle each individual request provides concrete, empirical confidence that scaling simplicity has genuinely been achieved, rather than merely assumed.

Common mistakes

  • Storing session state in a replica's own local, in-memory storage, breaking correctness the moment more than one replica is running simultaneously.
  • Writing shared data to a replica's own local filesystem and later expecting to read it back from a potentially different replica.
  • Building application logic that assumes a fixed, specific instance identity or a known, unchanging set of replicas rather than treating every replica as an interchangeable, anonymous copy.
  • Relying on a job queue or task distribution mechanism without confirming it actually provides the atomic dequeue or locking guarantee needed to prevent duplicate processing across multiple worker replicas.
  • Assuming an application is genuinely stateless and scaling-ready based on code review alone, without actually testing its behavior under a real, multi-replica deployment.

Scaling simplicity is achieved entirely through application design, externalized session state, externalized shared data storage, request-level statelessness, and correctly coordinated background job distribution, since the orchestration command needed to actually add more replicas is never the difficult part; the difficulty, and the actual engineering work required, lies entirely in making the application itself genuinely correct and consistent once more than one replica is running at the same time.