Case Study: API and Database
Requirements
- Serve an authenticated HTTPS API with predictable latency for a mobile client.
- Store relational data with transactions — accounts, ownership, referential integrity.
- Survive an instance restart, a deploy and a routine patch without a user-visible outage.
- Recover from an accidental data deletion, with a stated and tested recovery point.
- Be operable by two engineers who also write the application.
Deliberately not requirements
Half of a design is what it refuses to do. These are the refusals.
- No multi-region: a regional failure is a documented, accepted outage at this stage.
- No container orchestration: the workload is one service, and a control plane would be more software than the product.
- No independent scaling of subsystems, because there are no subsystems yet.
How the design got here
In order. Each stage leads with the problem that forced it.
One machine
The requirement. An API and a PostgreSQL instance on a single VM with a public address and a DNS record serve a few hundred users comfortably, deploy in one step, and cost the price of one machine. This stage is not a strawman: for an internal tool or a product still looking for users, it can be the correct final design for a long time.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| One VM running both the API and the database. | Two engineers, one deployable, no network hop between application and data. The operational surface is a single machine you can reason about entirely. | A managed database from day one, which costs more and removes an entire category of work — a defensible starting point if the budget allows. | You own patching, backups, tuning, disk sizing and recovery. Most importantly you own the fact that the application and the database compete for the same CPU and the same page cache, which makes performance problems harder to attribute. |
| A nightly dump copied to object storage. | It is the cheapest thing that satisfies "recover from an accidental deletion", and it is enormously better than nothing. | Point-in-time recovery via continuous archiving, which is what you actually want and which is more machinery than this stage warrants. | The recovery point is up to 24 hours, and — the part that matters — nobody has ever restored it. Until a restore has been performed, this is a file, not a backup (§98). |
Load balancer, two instances, managed database
A kernel security update required a reboot. The API was down for four minutes in the middle of the working day, and PostgreSQL came back needing recovery because the shutdown had been less graceful than expected. In the same month a disk-full event took the API down as well, because the application log and the database data shared a volume. One machine means one restart is an outage, and it means every resource is shared with everything else.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| A load balancer with two instances in two zones. | It converts "reboot the machine" from an outage into a routine operation, and it gives DNS a stable target that survives instance replacement. | Two instances with DNS round-robin, which costs nothing extra and cannot health-check, so a dead instance keeps receiving half the traffic until a TTL expires. | A new component with its own hourly cost, its own configuration, its own idle timeout, and a health check that will eventually be wrong in an interesting way. The application must also become stateless for this to mean anything. |
| Move the database to a managed service with a multi-zone standby. | Backups, patching, replication and failover are the provider's to run, and they are the tasks a two-person team does worst and least often. Failover in particular is not something you build correctly in an afternoon. | Self-hosted PostgreSQL on its own VM with your own replication, which is cheaper per gigabyte and appropriate when you have deep operational expertise or unusual extension requirements. | Roughly double the database spend for the standby, a per-write cross-zone latency cost from synchronous replication, and — the part people forget — you still own the schema, the indexes, the queries, the connection limits, the capacity choice and the bill (§57). Managed moved the boundary; it did not remove your half. |
| Instances become immutable and stateless. | Once two instances exist, any local state is a correctness bug waiting for a load balancer to expose it, and any hand-edited configuration is a difference between machines nobody has recorded. | Configuration management on long-lived instances, which is a legitimate model with a real ecosystem and a much longer path back to a known-good state. | Every change is a rebuild-and-replace, so the image pipeline becomes load-bearing and image build time becomes deployment latency. Debugging by SSH stops working, which is the adjustment engineers resent most. |
Private subnets, NAT, security groups
A routine scan found the database endpoint answering on 5432 from the internet, and the database log showed authentication attempts from three continents within a day of the endpoint being created. Nothing had been breached; the only thing standing between a password and the customer table was the password.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Workloads move into private subnets; only the load balancer and the NAT gateways stay public. | It removes direct internet reachability from every component that has no reason to be reachable, so an attacker must first defeat something you designed to be attacked. | Keep everything public and rely on security groups alone, which is genuinely defensible — a security group is the enforcement point either way — and leaves one misconfigured rule between the internet and your data. | Every private workload now needs a NAT to reach anything outbound, which is a per-zone hourly charge plus per-gigabyte processing, and debugging gets harder: you need a bastion, a session manager or a VPN to reach a machine at all. |
| Security groups reference other security groups, not IP ranges. | The rule then expresses the intent — "the database accepts connections from application instances" — and keeps being true as instances are replaced and addresses change. | CIDR-based rules, which are necessary for anything outside the VPC and go stale the moment a subnet is resized. | It is provider-specific behaviour rather than a general networking concept, and the rules read less obviously to someone who has only ever configured a firewall by address. |
| One NAT gateway per zone, not one shared. | A single NAT is a single point of failure for every outbound call in every zone, and it fails in the most misleading way available: health checks stay green while every external call times out. | One NAT for the whole VPC, which halves the fixed cost and is a common, deliberate choice for non-critical environments. | A second hourly charge and a second thing to monitor. This line item is deleted in cost reviews more often than any other on this list — write the reason next to the number. |
Secret manager and workload identity
The database password was set by hand as an environment variable on each instance. Rotating it meant editing every machine; one was missed and served errors for an hour. Then a new engineer found the same password in a deploy script in the repository, where it had been since the first week and was visible to everyone who had ever cloned it.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| The instance carries an identity, not a credential. | A workload should not borrow a human's key or hold a static one (§60). An identity attached to the instance produces short-lived credentials automatically, so there is no string to leak, to rotate or to commit. | A long-lived access key injected at deploy time, which works everywhere and is a permanent, un-expiring liability sitting in whatever system injected it. | Local development needs a documented path that is not "use the production credential", and the identity mechanism is one of the most provider-specific parts of any design — portability suffers, honestly and unavoidably. |
| The application fetches the database credential at startup and refreshes it. | It makes rotation an operation instead of an incident, and it removes the credential from images, environment variables and process listings. | Inject the secret as an environment variable at deploy time, which is far simpler and means rotation requires a full redeploy of every instance. | The secret manager becomes a hard dependency in the startup path, so it needs a private endpoint, a cache and a sensible behaviour when it is briefly unavailable. An application that cannot start because a secret fetch timed out is a new failure mode you just bought. |
| Encryption at rest with a customer-managed key. | It puts key usage in the audit trail and makes revocation possible, which is the difference between encryption as a checkbox and encryption as a control. | Provider-managed keys, which are simpler, cost less and are entirely adequate for many threat models. | Key lifecycle becomes yours: a deleted or disabled key is unrecoverable data, and keys are regional, which quietly constrains any future cross-region plan. |
Metrics, logs and alerts
A customer reported an outage on social media forty minutes before anyone internal noticed. The instances had been failing readiness for most of that time; nobody was looking, because nothing was configured to look.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Separate liveness from readiness. | Liveness answers "should this process be restarted?" and must stay shallow; readiness answers "should this instance receive traffic?" and may check dependencies. Conflating them means a two-second database blip restarts your entire fleet. | A single health endpoint, which is one less thing to explain and is wrong in both directions at the worst possible moment. | Two endpoints, two configurations, and a genuine risk that a deep readiness check ejects every instance simultaneously when the shared dependency wobbles. It needs a floor. |
| An external synthetic check over real TLS. | It is the only monitor that exercises DNS, the internet path, the certificate and the application together — and the only one that still works when the region it watches is gone. | Internal health checks only, which are cheaper and cannot see any failure that happens at or beyond the perimeter — expired certificates included. | A third-party dependency, a small bill, and alert tuning so that ordinary internet weather does not page anyone at 3am. |
| Ship logs off the instance immediately. | Logs on a machine that is gone are not logs, and immutable instances are replaced constantly. It also stops log growth from filling the volume the application runs on. | Local files with rotation, which is free and forfeits every investigation involving a terminated instance. | Log ingestion is billed by volume and is one of the most reliably underestimated line items in cloud infrastructure. Retention becomes a deliberate decision about how long your investigation window is. |
Backups, point-in-time recovery, and a rehearsed restore
A migration dropped a column in production at 14:00. The attempted restore revealed that the nightly dump had been failing for eleven days, because the job's role had lost a permission during an unrelated cleanup and the failure was recorded only as a non-zero exit code in a scheduler nobody watched.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Point-in-time recovery, not just nightly snapshots. | The failure you actually get is "someone ran the wrong statement at 14:00", and a nightly snapshot answers that with "we lose today". Continuous log archiving lets you stop the clock one second before the mistake. | Daily snapshots alone, which are far cheaper in storage and give you an RPO of up to a day. | Continuous archive storage that grows with write volume, a retention window you must choose and pay for, and a restore procedure with more steps — which is exactly why it must be rehearsed. |
| Copy backups to a second region and a separate trust boundary. | A backup in the same region and the same account as the thing it protects shares a failure domain with it — including the failure where credentials are compromised and everything is deleted together. | Same-region backups only, which are cheaper, faster to restore from, and useless in precisely the two scenarios you keep backups for. | Cross-region transfer and duplicate storage cost, plus key management across regions — an encrypted backup whose key is regional is not a cross-region backup. |
| Alert on the absence of a successful backup, not on the failure of the backup job. | This exact incident was a job that failed in a way nothing was listening for. "An object matching this pattern must exist by 03:00, or page" catches every cause, including the ones you have not imagined. | Alert on job exit status, which catches the common case and misses the job that was silently descheduled, deleted, or never ran at all. | A monitor that must be kept in step with the schedule; a stale expectation becomes a false page, and two false pages are enough to get it muted. |
| A monthly rehearsed restore that records elapsed time. | A backup you have never restored is not proven recovery (§98). The rehearsal is also the only honest source of your recovery time objective — the number you would otherwise be guessing in front of an executive. | Trust the provider's snapshot status page, which reports that a snapshot exists and cannot tell you whether it restores into a working system. | A recurring cost in engineering hours and in the scratch instance it restores into, producing no feature. It is the highest-value hour on this list and the first one cut when the roadmap is full. |
What would break this
Every design has a load, a failure or an organization size at which it stops being the right one.
- Write throughput past what one primary can absorb. Read replicas buy time for reads and do nothing for writes; past that you are partitioning data, which is an application redesign, not an infrastructure change.
- Work that does not fit in a request: report generation, bulk imports, third-party calls that take a minute. Doing it inline holds a web worker and eventually times out at the load balancer — this is the problem that creates
cs-worker-pipeline. - Traffic that varies by more than about 3x across the day. Fixed instance counts mean paying for the peak overnight and still being short at the morning spike; that is the autoscaling stage of
cs-saas-platform. - A second team and a second service. One deployable behind one load balancer is a good design for one team; the coordination cost of shared deploys is what eventually justifies separate services and, much later, a platform to run them on.
- A regional outage becoming unacceptable to the business. That is a different design and a different budget — see
break-regionfor what the honest version of that conversation looks like. - File uploads. The moment users upload anything, instance-local disk is wrong and object storage with direct uploads is right — a small change made much larger by doing it late.
Cost shape
Drivers and relative weights. Never a price.
Bars are relative weights, not currency. Real rates depend on provider, region, commitment and volume.