High-Level Design and the API
In one line: the high-level design is deliberately three boxes, which makes it easy to draw and easy to attack. The API is where the version-on-name-collision behaviour shows up — a detail that reveals how the system handles concurrency.
What this design cannot do — worth volunteering before the interviewer asks
Three boxes is the right starting point and it leaves everything open:
- Nothing decides where a blob goes. With many disks and many frontends, who chooses the target, and how does a reader find it again?
- Nothing tracks metadata. Container membership, access levels, versions — none of it has a home.
- Nothing handles a blob bigger than a disk. A 4 GB video on a full disk has nowhere to go.
- Nothing replicates. Lesson 3 demanded data persist until explicitly deleted; one disk failure violates that.
Every component the next lesson adds — the manager node, metadata storage, chunking, replication — answers one of these. Naming the gaps yourself is how you move into the detailed design under your own steam.
API design
The following operations require a registered and authenticated user. Registration and authentication are omitted for brevity.
Containers
createContainer(containerName)
| Parameter | Description |
|---|---|
containerName | The name of the container. It should be unique within a storage account |
deleteContainer(containerPath) listContainers(accountID)
| Parameter | Description |
|---|---|
containerPath | The path to the container the user wants to delete |
accountID | The ID of the user who wants to list their containers |
Blobs
putBlob(containerPath, blobName, data)
| Parameter | Description |
|---|---|
containerPath | The path of the container to upload into. Consists of the accountID and containerID |
blobName | The name of the blob. Should be unique within a container — otherwise the system gives the blob uploaded later a version number |
data | The file the user wants to upload |
getBlob(blobPath) deleteBlob(blobPath) listBlobs(containerPath)
| Parameter | Description |
|---|---|
blobPath | The fully qualified path of the data or file, including its unique ID |
containerPath | The path to the container to list blobs from |
deleteBlob marks a blob for deletion. The system removes the actual data asynchronously during garbage collection.
Note: this API definition is logical. In production, large data transfers may require multistep streaming.
Note: blob retrieval APIs provide metadata such as size, version number, access privileges, and name.
Name collisions create a version rather than failing or overwriting — that's a real design choice
putBlob with an existing blobName does not error and does not overwrite. It creates a new version.
That follows directly from Lesson 1's immutability. If blobs cannot be modified in place, an "overwrite" has to mean "store new bytes and point the name at them," and keeping the old bytes addressable by version costs nothing extra — they already exist as separate chunks.
Three consequences worth naming:
No lost updates. Two clients uploading the same name concurrently both succeed; neither silently destroys the other's data. Lesson 7 covers how the manager node serializes them.
Delete becomes the dangerous operation, not overwrite. In most systems an accidental overwrite is unrecoverable. Here it is just an older version — which is why the retention window from Lesson 3 is attached to deletion specifically.
Storage grows with writes, not with distinct names. Uploading the same blob a thousand times stores a thousand versions and bills for all of them, which is exactly what Lesson 2's lifecycle expiration rules exist to bound.
The upload contract the logical API hides
This API definition is logical. In production, large data transfers may require multistep streaming.
That one line hides the mechanism every real object store exposes and every interviewer asks about. Two pieces: the bytes must not go through your servers, and a large upload must survive a dropped connection.
The bytes bypass your servers
The obvious implementation has the client POST the file to an application server, which forwards it to storage. At blob-store scale that is wrong, and not marginally.
This is what "access a blob via a system-generated URL" from the requirements is for. Because the system generates the URL, it can sign it — and a signature can carry far more than a location.
A presigned URL is a cryptographic hash over the request details — method, object path, expiry — computed with the account's secret key. The storage service recomputes the same hash on arrival and compares. Two consequences worth knowing:
Generating one costs nothing. It is a hash computed in the application's own memory; there is no round trip to the storage service to mint it. So handing out a URL is cheap enough to do per request.
Conditions are baked into the signature, so they cannot be edited by whoever holds the link. The two that matter are a size range — without it, a URL meant for a 5 MB avatar accepts a 5 TB upload and your storage bill with it — and a content type, so an image endpoint cannot receive an executable.
The URL is a capability: whoever holds it can use it, which is why it expires in minutes rather than days.
Large uploads: parts, not one PUT
A 5 GB upload over a 100 Mb/s connection takes about seven minutes. A single PUT that fails at 99% starts over.
Four properties fall out of this, and they are the reason every provider offers it:
- Resumability. A dropped connection costs one part, not the file.
- Parallelism. Several parts in flight saturates the link.
- Progress. Parts completed over parts total is a progress bar, for free.
- Integrity. The per-part ETag lets the service verify it assembled the bytes the client actually sent.
Two operational details worth volunteering. Until the completion call succeeds there is no object — only parts, which are not readable. And incomplete uploads are billed, so an abandoned upload leaks storage forever unless a lifecycle rule deletes parts after a day or two. That is a nice callback to the previous lesson: the same lifecycle machinery that demotes cold data also sweeps up failed uploads.
deleteContainer is the operation to be careful about
deleteContainer removes a container and all blobs inside it — potentially millions of objects in one call.
Two things fall out. It cannot be synchronous: marking millions of blobs and reclaiming their chunks is garbage-collector work, so the call returns after marking the container and the rest happens in the background.
And it is the highest-blast-radius operation in the API, which is a good argument for the retention window applying at container level too. "Delete container" typed against the wrong container is a genuinely catastrophic, genuinely common mistake.
Key takeaway
Three components to start: clients, frontend servers, storage disks — with no placement logic, no metadata, no chunking, and no replication, all of which the detailed design adds. Seven API calls, where a name collision produces a version rather than an overwrite, and both delete operations are asynchronous by necessity.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "Clients talk to frontend servers which write to disks. The API is put, get, delete, and list." |
| L5 | Volunteers the gaps: "this has no way to decide which disk a blob goes to, no metadata store to find it again, and nothing handles a blob larger than one disk." |
| Staff+ | Reads the versioning behaviour: "a name collision creates a version rather than overwriting, which follows from immutability — so there are no lost updates, and delete rather than overwrite becomes the dangerous operation. I'd also expect multipart upload in practice: single-shot put isn't resumable and can't parallelize, and chunking the wire protocol lines up with how we're going to chunk server-side anyway." |
Next: the components that fill in the gaps.