Scaling software, one boundary at a time.
An inventory ordering platform is a small enough system to understand—and a demanding enough system to expose what scaling actually requires.
Imagine two customers ordering the last available stock at the same time. Now imagine one customer's connection drops after the order is saved, so their browser retries. Adding another server gives us more capacity to process both requests. It also gives us more opportunities to get the answer wrong.
I built base0, an inventory ordering experiment with a Purple storefront, to explore how infrastructure components fit together. The useful lesson is that scale begins with the promises a system can keep under concurrency, retries, and partial failure.
01 / Give each service a clear responsibility
base0 separates authentication, orders, order summaries and receipts, and payments into four backend services. Each has its own GraphQL schema, business logic, repositories, and PostgreSQL database. The databases currently share one PostgreSQL container, so their ownership is separate while their underlying compute and failure domain remain shared.
No service directly reads another service's database. When the payment service needs an order, it goes through the owning service. That boundary keeps business rules close to the data they protect and gives future changes a defined interface.
There is a cost: a single user action can depend on several network calls. Protected operations depend on authentication, and order creation obtains a quote through the summary service. A slow dependency can delay its callers. Separating services creates options for independent scaling, but those options become useful only when we understand the dependency chain.
02 / Make the API contract explicit with GraphQL
The browser uses service-specific GraphQL endpoints behind nginx. This is four explicit schemas, not a federated graph. Internal REST routes remain available for service-to-service calls. For example, the catalog can request precisely the product fields its screen needs:
query Catalog {
products {
id
name
price
min
stock
}
}
This query goes to /api/orders/graphql. Mutations such as placeOrder, confirmPayment, and issueReceipt make state changes visible in the contract. Reading a receipt is a query; creating one is a mutation. The backend API notes document the operations and request headers.
GraphQL controls the response shape; it does not automatically make database access cheap. Resolvers still need bounded work and authorization. base0 limits payload size, tokens, nesting, and expanded selections. Its frontend also checks the GraphQL errors array because an HTTP 200 response can still contain an execution failure.
Lesson: treat the schema as a product contract and the resolver as a resource budget. Measure database and downstream work behind each operation before assuming a smaller JSON response means a faster system.
03 / Protect correctness before adding capacity
Return to those two customers competing for stock. base0 locks the relevant product rows, checks stock and prices, decrements inventory, and stores the order and item snapshots in one transaction. If a check fails, the transaction rolls back. The implementation visits products in a consistent order, which helps reduce deadlock risk when orders overlap.
A second protection handles retries: an idempotency key identifies the customer's order attempt. Repeating the same request with that key returns the existing order. Reusing it for a different request produces a conflict. A transaction-scoped advisory lock serializes concurrent attempts using the same user's key. These rules live in the order repository, where they apply across application processes.
Payment and fulfillment are separate states. A successful simulated payment does not mean goods have been delivered. Receipts require successful payment and matching amount and currency. Those distinctions make the interface honest while leaving room for future fulfillment work.
There are still lifecycle questions to solve. Stock is reserved when an order is placed, but cancellation and reservation expiry remain future work. At higher volume, abandoned orders could tie up inventory. More replicas cannot decide when that stock should be released; the business workflow needs an explicit policy.
04 / Make the system reproducible with Docker
The Docker Compose configuration assembles the storefront containers, four backend APIs, and PostgreSQL. nginx gives the browser one origin on port 8080, while backend and database ports stay inside the container network. Each API can use internal port 8000 because containers have separate network namespaces.
Named storage preserves database contents across container restarts. Health checks test readiness of dependencies, and numbered migrations run under a database lock at service startup. This turns setup assumptions into a repeatable environment that can be inspected as a whole.
Lesson: reproducibility is a prerequisite for meaningful scaling experiments. Establish a stable baseline, then change one variable—workers, traffic, or database capacity—and compare the result. Several containers on one machine still share that machine's limits.
05 / Move independent work onto Kafka
A future version might need notifications, analytics, and fulfillment preparation after an order is placed. Those tasks need not all delay checkout. I would consider Kafka when durable events, replay, and multiple independent consumers justify the operational cost.
A proposed flow is: commit an order and an outbox record together, let a publisher send an OrderPlaced event to Kafka, and have downstream consumers process it. The outbox closes the gap where a database write succeeds but the process crashes before publishing. It would be new implementation work in base0.
Consumers must handle duplicate delivery: record an event ID alongside their local changes and make retries safe. Keying events by order ID can keep an order's events in one partition; it does not establish global ordering. Kafka's delivery semantics also explain why exactly-once processing does not automatically extend to an external side effect such as sending an email.
Lesson: asynchronous work trades immediate coordination for explicit eventual consistency. Keep the stock reservation in its transaction, show downstream progress honestly, and define retry and recovery behavior before moving work off the request path.
06 / Operate replicas with Kubernetes and AWS
Once measurements justify more application instances, Kubernetes becomes a possible operating layer. Its Deployments manage replicas and rolling updates. A future base0 deployment would also need readiness probes, resource requests and limits, configuration, secrets, and a deliberate approach to database migrations.
Adding API replicas increases potential database connections and downstream calls. If PostgreSQL is already saturated, that can worsen latency. Autoscaling needs a capacity budget across the entire request path, and database durability needs its own design.
For an AWS version, Amazon EKS could operate the Kubernetes layer. If the event-streaming use case becomes real, Amazon MSK provides managed Apache Kafka. These are possible deployment choices, not services currently provisioned by this repository.
I would first define the workload, recovery objectives, and operating budget, then choose hosting and database arrangements around them. Managed services still leave application teams responsible for access policies, observability, recovery drills, and understanding failure. A learning deployment may need a much simpler setup than a busy production platform.
07 / Measure the next bottleneck
The next experiment should answer a specific question: what limits successful order throughput while preserving inventory correctness? I would run a repeatable workload and record:
- User experience: successful orders per second, p95 latency, and error rate.
- Contention: database lock waits, connection usage, and query duration.
- Correctness: duplicate orders, oversold stock, and inconsistent payment or receipt state.
- Recovery: behavior after an API restart, a timeout, or a database interruption.
If Kafka is added, consumer lag and retry counts join those measurements. If Kubernetes is added, replica count and resource usage must be read alongside database pressure. Change one part of the system and check whether the customer's outcome improves.
base0's repository includes tests for concurrent oversell prevention, idempotency conflicts, and transaction rollback. Those checks are a starting point for correctness; a load test and an operational recovery exercise answer different questions. There are no measured throughput claims in this lesson.
My main takeaway from base0 is to grow the system around its guarantees. GraphQL names the contract, transactions protect the order, and Docker makes the environment repeatable. Kafka, Kubernetes, and AWS become useful when the next measured problem calls for the capabilities they provide.