all_lessons/data_intensive_systems/24 · cloud storagelesson 25 / 35 · ~15 min

Part 10 · Cloud / AI systems

Cloud-Native Storage as a Subsystem

Every mechanism so far — the write-ahead log, the LSM-tree's sequential writes, the leader shipping its replication log to followers — quietly assumed a disk: a block device attached to the machine, where a write lands in microseconds and you can overwrite a byte in place. In the cloud that assumption is gone. The disk is now a network service: storage lives on the far side of a network, compute is rented by the hour and may not survive the night, durability is a contract someone else signs, and moving a byte across a region boundary has a price tag. This lesson treats cloud storage not as "the same thing but managed", but as a subsystem with its own latency, failure, and cost model — and shows how that model reshapes choices you made in earlier lessons.

Source grounding
Original synthesis inspired by Martin Kleppmann, Designing Data-Intensive Applications — and specifically the 2nd-edition additions on the cloud era: the separation of storage and compute, object storage as the durable substrate, cloud data warehouses, and the operational delegation of managed databases. The mechanisms (durability, consistency, placement) are the same ones built earlier in this track; here we re-read them through a cloud lens. Built here, not reproduced.
Linear position
Prerequisite: Lesson 04 (storage engines: the LSM-tree and columnar layout, and why sequential writes and immutable files matter) — you know that a storage engine is shaped by the cost of writes and the cost of overwriting in place. Lesson 08 (replication: leaders, followers, lag, failover) — you know copies are placed across machines for availability and that copies disagree across time.
New capability: Reason about cloud storage as a subsystem with its own latency profile, durability contract, consistency model, failure boundaries (control-plane vs data-plane), and cost model (egress, per-op). Decide between an object store, a cloud warehouse, and a managed database for a given workload; explain how a lakehouse table format (Iceberg/Delta/Hudi) builds ACID tables, snapshots, and time travel on a store with only single-object atomicity; and place data across regions and availability zones for latency and blast-radius isolation.
The plan
Six moves. (1) Object stores (S3, GCS, Azure Blob) as the new durable substrate — what "the disk is a service" actually changes: storage/compute separation, the durability contract, consistency, and a latency profile so different from a local SSD that it reshapes storage-engine design (lesson 04). (2) Cloud warehouses (Snowflake, BigQuery) — compute that scales independently of stored bytes (lesson 21). (3) Managed databases (RDS, Aurora, Spanner, DynamoDB) as operational delegation: what you hand off and what you give up. (4) The constraints that are unique to running on someone else's cloud — egress cost, IAM, regions and availability zones (lesson 08). (5) The two failure boundaries — control plane vs data plane — and serverless / scale-to-zero. (6) A worked egress number, where truth lives, and the hand-off.

1 · The disk is now a network service: object stores

What this is: S3 / GCS / Azure Blob as the durable substrate, and the four ways they differ from a local disk.

An object store (Amazon S3, Google Cloud Storage, Azure Blob Storage) is a service that stores named objects — opaque blobs of bytes, from a few KB to terabytes — addressed by a key (bucket/path/to/object), reached over HTTP. It is not a filesystem and not a block device. You PUT a whole object and GET a whole object (or a byte range of one); you do not seek to offset 4096 and overwrite 64 bytes. That single restriction — objects are immutable; there is no in-place update — is the hinge the rest of this section turns on.

The first and biggest consequence is the separation of storage and compute. On a classic database server, the disk is bolted to the CPU: if you want more storage you buy a bigger machine, and that same machine also does the computing. With an object store, the bytes live in a service that any number of compute nodes can read from over the network. Storage scales by writing more objects; compute scales by renting more (stateless) machines that read those objects. The two grow on independent axes. This is the architectural fact underneath cloud warehouses (§2) and the lakehouse pattern, and it is why a query engine can spin up 100 workers for one minute, read a dataset none of them owns, and spin back down.

CLASSIC SERVER (storage bolted to compute) CLOUD (storage and compute separated) ┌───────────────────────┐ compute (stateless, ephemeral, rented) │ CPU + RAM │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ ─────────────── │ │ node │ │ node │ │ node │ │ node │ ← scale this │ local SSD (the disk)│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ by the hour └───────────────────────┘ │ │ │ │ grow storage = bigger box └────────┴───┬────┴────────┘ the box does everything HTTP GET/PUT │ ┌────────────▼────────────┐ │ OBJECT STORE (S3) │ ← scale this │ immutable objects, │ by writing │ replicated across AZs │ more objects └─────────────────────────┘

The second consequence is the durability contract. The object store stores many replicas of each object across multiple availability zones (AZs — physically separate data centers within a region, with independent power and networking; introduced in §4). Providers advertise figures like "eleven nines" of durability — 99.999999999% — meaning the modeled annual probability of losing a given object is around 1 in 100 billion. Treat that number as a marketing summary of "we keep enough geographically separated replicas and continuously repair them that you should not plan to lose objects", not as a guarantee you can verify. Durability here is delegated: you no longer run the replication and anti-entropy you built in lesson 08 — the provider does, behind the API. What you give up is the ability to inspect or prove it; what you gain is not having to.

The third consequence is consistency. For years S3 was eventually consistent: PUT an object, immediately GET it, and you might get the old version or a 404, because the read hit a replica that hadn't caught up — exactly the replication-lag anomaly from lesson 08, now inside the storage service. As of 2020, S3 provides strong read-after-write consistency: after a successful PUT of a new object (or an overwrite), any subsequent GET or LIST sees that write. This removed a whole class of bugs (and the read-after-write workarounds people built), but note the boundary: it is read-after-write on a single object; it is not a multi-object transaction. You still cannot atomically swap two objects, which is why table formats (§ below) need their own commit protocols.

The fourth consequence — the one that reshapes storage-engine design — is the latency and throughput profile. A local NVMe SSD answers a small read in roughly 100 microseconds (0.1 ms). An object-store GET is an HTTP request over the network: first-byte latency is tens of milliseconds — call it 20–100 ms, often ~50 ms. That is 100× to 1000× slower per operation. But the throughput story is the opposite: you can run thousands of parallel GETs and pull many GB/s aggregate, far more bandwidth than one SSD. So the object store is a high-latency, high-bandwidth, immutable, whole-object device. That profile dictates how you store data on it:

Local SSD assumes…Object store forces…
Cheap small random reads (~0.1 ms)Few, large, sequential reads — amortize the ~50 ms per-op cost over many MB; read big byte ranges, not 4 KB pages
In-place overwrite of a pageWrite new immutable objects; never overwrite — exactly the LSM-tree / append-only instinct from lesson 04
One process owns the diskMany readers, no single owner; metadata/commit must live elsewhere

This is why the modern data lake stores Parquet (the columnar format from lesson 04) as immutable objects, and why an LSM-tree on an object store flushes large immutable SSTables (which map perfectly onto immutable objects) rather than churning small pages. The append-only, "never overwrite, compact in the background" design you learned for write-heavy local engines turns out to be exactly what an object store wants — the immutability that was a clever optimization on a disk is a hard requirement in the cloud. And because no single process owns the bucket, the "which objects make up the current table" question can't live in a local index; it needs an external commit log. That is what table formats like Iceberg, Delta Lake, and Hudi provide: a small, atomically-updated manifest that points at the current set of immutable data objects — giving you snapshots and atomic table updates on top of a store that only guarantees single-object writes. This pattern — the lakehouse — is one of the biggest things the cloud era added to the field, and it is worth seeing the mechanism, because it is built entirely from primitives you already have.

How a table format buys ACID tables from a store with only single-object atomicity

The bind: a table is thousands of immutable Parquet objects, but appending a day's data means adding (and a compaction means replacing) many objects at once — and the store guarantees atomicity for one object only (§1's consistency point). A reader running a LIST mid-write would see a half-committed table. The fix is the same move the B-tree made with its WAL (lesson 03) and the same MVCC snapshot the database made for isolation (lesson 13), now lifted onto object storage:

data files (immutable Parquet objects, never edited in place) part-0001.parquet part-0002.parquet ... part-9999.parquet a MANIFEST lists exactly which data files belong to one SNAPSHOT (= one table version): snapshot 7 -> manifest_7 -> { part-0001 ... part-5000 } snapshot 8 -> manifest_8 -> { part-0001 ... part-5000, part-5001 ... part-5040 } (added a day) CURRENT-POINTER ---> snapshot 8 (a single tiny object / catalog row / log entry) commit = (1) PUT new immutable data files [many writes, nobody sees them yet] (2) PUT a new manifest for snapshot 8 [still invisible] (3) atomically swap CURRENT-POINTER 7 -> 8 [ONE atomic op == the commit]

Step 3 is the whole trick: because flipping the current-pointer is a single atomic operation — a single-object compare-and-swap, or an ordered append to a tiny transaction log (Delta's _delta_log/ of numbered JSON commits), or a catalog row update (Iceberg) — a reader sees either snapshot 7 or snapshot 8, never a half-written set. You bootstrapped multi-file atomicity from the one-object atomicity the store gave you, exactly as the WAL bootstrapped a recoverable B-tree from torn page writes. Three capabilities then fall out for free:

The bill is predictable from the spine: thousands of tiny commits breed metadata bloat and a small-file problem (each commit may add a few small objects, and ~50 ms-per-GET latency punishes many small reads), so table formats need exactly the lesson 03/04 cures — background compaction to merge small files into large ones, plus snapshot expiry to delete the manifests and now-unreferenced data files of ancient versions (which also bounds how far back time travel reaches). The three formats differ mainly in commit-log shape and how they handle row-level updates — Delta's ordered JSON log, Iceberg's manifest/snapshot tree + catalog, Hudi's upsert-oriented copy-on-write vs merge-on-read — but the core move, an atomic pointer swap over immutable files, is identical.

2 · Cloud warehouses: compute scales apart from data

What this is: Snowflake / BigQuery built on storage-compute separation.

A cloud data warehouse (Snowflake, Google BigQuery, Amazon Redshift in its modern form) takes §1's separation and makes it the product. Your tables live as columnar objects in the object store. To run a query you provision a compute cluster (Snowflake calls it a "virtual warehouse"; BigQuery hides it as on-demand "slots") that reads those objects, executes the analytical query (the scan/join/aggregate machinery from lesson 21), and returns a result. The cluster is ephemeral and sized on demand.

The payoff is that compute and stored data scale independently. You can store 500 TB and query it with a tiny cluster overnight and a huge one at 9am, paying only for the compute you spin up — the stored bytes cost the same regardless. Two teams can run heavy queries against the same data on two separate clusters without contending for each other's CPU, because they are different compute reading shared storage. This is the cloud-native answer to the OLTP/OLAP split from lesson 04 and the query-execution concerns of lesson 21: the warehouse is a query engine you rent by the second, sitting on top of the object store of §1.

3 · Managed databases: operational delegation

What this is: RDS / Aurora / Spanner / DynamoDB, and the trade you make by not running the database yourself.

A managed database is a database the cloud provider operates for you: Amazon RDS (managed Postgres/MySQL), Aurora (a cloud-rearchitected MySQL/Postgres that puts the storage layer on a distributed, multi-AZ service), Google Spanner (a globally-distributed, strongly-consistent relational database), DynamoDB (a managed key-value/document store). You still design the schema and write the queries; you do not run the machine. The deal is operational delegation — and like any delegation, it is a trade, not a free win.

You hand off (provider does it)You give up
Backups & point-in-time recovery — automatic snapshots + log retention so you can restore to any second in a windowControl — you can't touch the OS, pick arbitrary extensions/versions, or tune the storage layer below the provider's knobs
Patching & upgrades — security patches and minor-version upgrades applied in maintenance windowsPortability — Aurora, Spanner, DynamoDB have proprietary surfaces; migrating off is a real project (a form of lock-in)
Failover — multi-AZ standby promotion (the lesson-08 failover, run for you, often in under a minute)Cost — you pay a managed premium per hour, and at very large scale self-hosting can be markedly cheaper
Replication & scaling — read replicas, storage auto-grow, sharding (DynamoDB) behind the APITransparency — when it's slow or fails, you debug through the provider's metrics and a support ticket, not the box

The decision is rarely "managed vs not" globally; it is per system. A small team running a product database almost always wins by delegating — failover and backups done right are hard, and that is exactly the lesson-08 machinery you don't want to operate at 3am. A team at scale with a hot, predictable, cost-sensitive workload may self-host to recover the managed premium and the control. Name the trade explicitly: what operational burden are you offloading, and what control / portability / cost are you paying for it?

4 · Constraints unique to the cloud: egress, IAM, regions and AZs

What this is: three cloud realities that become hard architecture constraints — money, identity, and placement.

Egress cost. Reading and writing within a region is cheap or free; moving data out is not. Egress — data leaving a region or leaving the cloud entirely (to the internet, to another cloud, sometimes to another region) — is billed per GB, and it is one of the largest and most surprising lines on a cloud bill. This is not an accounting footnote; it is an architecture constraint. It is why you put compute in the same region as its data (so the bytes never cross a priced boundary), why cross-region replication has a real recurring cost, and why "just back up everything to another cloud" is expensive at scale. Egress quietly pushes architectures toward keeping data and the things that read it co-located. We put a number on this in §6.

IAM (identity and access management). In the cloud, identity is first-class infrastructure. Every API call — a service reading an object, a function writing a row — is made by a principal (a user, a role, a service account) and is allowed or denied by an IAM policy. The discipline is least privilege: each principal gets exactly the permissions it needs and no more, so a compromised service can touch only its own bucket, not the whole account. This matters for data systems specifically because access control is no longer just rows-in-a-table permissions inside one database — it is who can read this bucket, who can decrypt this key, who can assume this role, enforced by the cloud before your code even runs. Misconfigured IAM (a public bucket, an over-broad role) is among the most common causes of real data breaches.

Regions and availability zones. A cloud is organized as regions (geographic areas — us-east-1, europe-west1) each containing several availability zones (AZs — physically separate data centers within the region, with independent power, cooling, and networking, connected by fast low-latency links). This is the cloud's placement vocabulary, and it serves the two goals from lesson 08:

REGION us-east-1 REGION eu-west-1 ┌─────────────────────────────────────┐ ┌──────────────────────────┐ │ AZ-a AZ-b AZ-c │ async │ AZ-a AZ-b AZ-c │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ replica │ ┌──────┐ ┌──────┐ │ │ │replica│ │replica│ │replica│ │ ──────────▶ │ │replica│ │replica│ │ │ └──────┘ └──────┘ └──────┘ │ (egress $) │ └──────┘ └──────┘ │ │ synchronous across AZs (cheap, │ │ disaster-recovery copy │ │ low latency) → survive 1 AZ loss │ │ → survive region loss │ └─────────────────────────────────────┘ └──────────────────────────┘ within region: free/cheap, ~sub-ms across region: priced egress + tens of ms latency

5 · The two failure boundaries: control plane vs data plane, and serverless

What this is: the most important cloud failure distinction, plus scale-to-zero compute.

Cloud services are split into two layers, and they fail independently:

The crucial property: the data plane can keep serving while the control plane is down. In several real large-scale cloud outages, already-running instances and existing objects kept serving reads and writes fine, but you could not launch new instances, scale up, or even reliably load the console. The architectural lesson is twofold. First, do not put control-plane operations on your critical request path — if serving a user request requires provisioning a resource or calling the management API, a control-plane outage takes you down even though the data plane is healthy. Second, your recovery plans often lean on the control plane (auto-scaling, launching replacement nodes, failing over to another region), so a control-plane outage can disable exactly the mechanism you were counting on to ride out a problem. Drill this failure explicitly (lesson 28): assume the console and provisioning are gone, and check that your already-running fleet still serves.

Serverless and scale-to-zero. The cloud's extreme of compute/storage separation is serverless compute (AWS Lambda, Cloud Functions, Cloud Run): you supply a stateless function, the platform runs it on demand, and when no requests arrive it scales to zero — you run (and pay for) nothing. Paired with serverless data (DynamoDB on-demand, Aurora Serverless, BigQuery), an entire system can idle at near-zero cost and scale up instantly under load. The costs are real and specific: (1) cold starts — the first request after idle pays the time to spin up a fresh execution environment (tens to hundreds of ms, more for heavy runtimes), so latency is bursty; (2) statelessness — a function keeps nothing between invocations, so all state must live in the external data plane (object store, managed DB), which makes the "where is truth?" question below even sharper; and (3) the per-request and per-op pricing rewards exactly the few-large-operations discipline §1 already forced on you.

6 · Worked number: the egress bill of a cross-region copy

What this is: turning the §4 egress constraint into dollars and milliseconds.

Suppose you keep a 40 TB training-data lake in us-east-1 and a stakeholder asks you to also stand up a copy in eu-west-1 for a European team — and to refresh 5 TB of new data into it every month. Cross-region egress is billed at roughly 0.02 dollars per GB (rates vary by provider and region pair; use this as an order-of-magnitude figure). Note 40 TB = 40,000 GB.

Initial copy: 40,000 GB × 0.02 $/GB = 800 $ (one time) Monthly refresh: 5,000 GB × 0.02 $/GB = 100 $/month → 1,200 $/year ───────────────────────────────────────────────────────────────── Year-1 egress just to MOVE the bytes: 800 + 1,200 = 2,000 $ (this is ON TOP of paying to STORE the second 40 TB copy in eu-west-1)

Two things to read off this. First, the movement is a recurring cost separate from storage: you pay to store the second copy and ~1,200 dollars/year just to keep it fresh — and if instead of replicating you let the EU team query us-east-1 directly, every query result also crosses the priced boundary, so a chatty analytics workload can quietly out-cost the replica. Second, latency: a cross-region round-trip is tens of milliseconds versus sub-millisecond within a region, so even ignoring money, serving EU reads from us-east-1 means every read pays a trans-continental hop. Egress cost and cross-region latency together are why you co-locate compute with data and treat a second-region copy as a deliberate, budgeted disaster-recovery / latency decision — not a default. Scale this to petabytes and a careless cross-region or cross-cloud data flow becomes a six-figure line item.

Failure modes

  • Control-plane outage. Console and provisioning are down; you can't launch instances, scale, or fail over — even though the data plane still serves. If recovery depends on the control plane, you're stuck. Drill it (lesson 28).
  • Treating the object store like a local disk. Small random GETs (4 KB at a time) at ~50 ms each murder throughput and rack up per-op charges; the fix is large sequential reads and immutable, append-only layout (lesson 04).
  • Assuming multi-object atomicity. S3 is read-after-write per object, not transactional across objects; a naive "write two objects" table update can be seen half-done. Use a table format's commit manifest.
  • Surprise egress bill. A cross-region or cross-cloud data flow (replication, chatty queries, a backup to another provider) silently bills per GB; it can dwarf storage cost.
  • Over-broad IAM. A public bucket or an over-permissioned role turns one compromised service into a full-account breach; least privilege is not optional.
  • Cold-start latency. Serverless idles to zero, so the first request after quiet pays the spin-up; latency-sensitive paths need warm capacity.
  • Silent lock-in. Proprietary managed surfaces (Aurora, Spanner, DynamoDB) make migration a project; you discover the portability cost only when you try to leave.

Decision checklist

  • Is the workload write-heavy ingestion, wide analytical scans, or transactional point reads? → object store + table format, cloud warehouse, or managed OLTP DB respectively.
  • Are storage and compute on independent scaling axes here, and have you co-located compute with its data to avoid egress?
  • What is the durability/consistency contract you actually need, and does the chosen service meet it (single-object read-after-write vs multi-object atomicity)?
  • What did you delegate (backups, patching, failover) and what did you give up (control, portability, cost)? Is that trade right for this system's scale?
  • How large a failure must this survive — one AZ (cheap, same-region) or a whole region (egress + latency cost)? Place replicas accordingly.
  • Does any critical request path depend on the control plane? If so, remove that dependency.
  • Is IAM least-privilege, with no public buckets and scoped roles per service?
  • Have you estimated egress for every cross-region / cross-cloud data flow before committing to it?

Checkpoint exercise

Try it
You are designing storage for an ML platform on a single cloud. There are three data systems: (a) a training-data lake of 200 TB of event logs and images, scanned in bulk by training jobs; (b) a low-latency online feature store read at inference time by model servers in two regions; (c) an experiment-metadata database (runs, metrics, model lineage) with transactional reads/writes from a dashboard. For each: pick object store / cloud warehouse / managed DB, justify it from the latency-throughput and durability/consistency profiles in this lesson, and say what you delegate. Then: the model servers in region B currently read the feature store in region A — estimate the egress and latency cost of that and propose a fix. Finally: write one sentence on what breaks if the cloud's control plane goes down for an hour, and whether your design still serves inferences.

Where this points next

This lesson handed durability, replication, and even failover to a provider — and the moment you do that, where your data physically lives and who is allowed to touch it stop being purely technical choices. A region is also a legal jurisdiction; an object store's deletion semantics are also a compliance obligation; an IAM policy is also an audit boundary. Lesson 25 takes that thread up directly: law, regulation, and ethics as an architecture input — data residency, the right to be deleted, retention requirements, and consent — treating these not as paperwork bolted on at the end but as constraints that shape the system the same way latency budgets and egress costs do.

Where is truth?
System of record: the object store (for the data lake) or the managed database (for transactional state) — the cloud service is now the authoritative copy. Copies / derived views: cross-AZ replicas (managed for you, invisible behind the API) and any cross-region disaster-recovery copy or warehouse-side cache. Freshness budget: single-object read-after-write is immediate; cross-region replicas lag by the async-replication budget (lesson 08); serverless reads always go to the live data plane. Owner: the team's cloud account, scoped by IAM least-privilege — identity is the access boundary. Deletion path: object lifecycle / retention policies and managed-DB retention windows — deletion is delegated to the provider's lifecycle rules, which is exactly where the lesson-25 compliance obligations attach. Reconciliation / repair: the provider's internal anti-entropy keeps the AZ replicas whole; you reconcile by re-running ingestion or restoring from a snapshot. Evidence it is correct: the durability contract (the eleven-nines model), point-in-time backups you can actually restore-test, and verified cross-AZ / cross-region replication — durability you can demonstrate by recovering, not just by trusting the number.
Takeaway
In the cloud the disk is a network service, and that one fact reshapes everything earlier lessons assumed. Object stores (S3/GCS/Blob) separate storage from compute, store immutable whole objects with delegated eleven-nines durability and (now) single-object read-after-write consistency, and have a high-latency (~50 ms/op), high-bandwidth profile that forces the append-only, large-sequential, never-overwrite design you learned for LSM-trees and Parquet in lesson 04. Cloud warehouses (Snowflake/BigQuery) make storage/compute separation a product: rent query compute by the second over shared columnar data (lesson 21). Managed databases (RDS/Aurora/Spanner/DynamoDB) are operational delegation — you hand off backups, patching, and failover (lesson 08), and give up control, portability, and some cost. Cloud-only constraints become real architecture: egress is a per-GB bill that pushes you to co-locate compute with data (40 TB cross-region costs hundreds to thousands of dollars just to move), IAM makes least-privilege identity the access boundary, and regions / AZs are how you place data for latency and blast-radius isolation. Critically, the data plane keeps serving while the control plane (provisioning, console, scaling) is down — so keep control-plane calls off the critical path and drill that outage. Serverless scales to zero at the cost of cold starts and forced statelessness. Truth now lives in a service you don't operate; durability is delegated and proven by restore tests, deletion runs through lifecycle policies — which is exactly where lesson 25's legal and ethical obligations attach.

Interview prompts