Day 57 of 100·System Design Day·477 words

Object storage vs databases, and S3 architecture

Why S3 is the way it is — and where your projects already use it correctly.

Concept → your projects

S3's actual architecture (worth knowing because it explains the constraints): objects are keyed; a partition map assigns key-prefixes to storage nodes (the reason prefix throughput limits exist — a hot prefix concentrates on one node); GET/PUT are object-granular; durability comes from replication/erasure coding within a region (11 nines); consistency is read-after-write per object (strong since Dec 2020; list-after-write still eventually consistent). It's not a file system: no partial writes, no locks, no queries — cheap, durable, infinitely-wide storage with an HTTP API.

The decision rule: object storage for blobs, databases for state. The classic mistakes: storing files in Postgres (TOAST bloat, buffer pollution, backup drag — though small files can be fine) vs. storing truth in S3 (no transactions, no constraint — your zoth policies must never live there).

Cost/latency shaping — the S3 request patterns that matter:

  • Multipart upload for big objects (parallel parts, retry only failed parts) — the pattern behind traverse's streaming recon dump (writes chunks, then rename/complete).
  • Range GETs for partial reads (video seek, tailing logs).
  • Prefix listing (ListObjectsV2) is O(prefix) — not a query, a scan.
  • Lifecycle rules to cheap storage classes (IA/Glacier) — the retention answer (Day 87's topic).
  • Cross-region replication for DR — traverse's absence of it for backups is "backups live in a different region from the primary" (already region-separated risk), a defensible mini-DR answer.

Your actual S3 usage (traverse) — correct, claimable:

  1. WAL archive (us-east-1, dedicated bucket/creds): object-per-WAL-segment, immutable, lifecycle-friendly. The canonical DB+object-storage pattern: database for state, object storage for the change log.
  2. Career applications: resume files → key applications/{id}/{filename}, in the careers bucket (ap-south-1) — user blobs never touch the database; the DB stores the key + metadata. Plus signed/limited access paths. Two buckets = two blast radii (Day 34) — also the "tenant separation" idea applied at the storage layer.
  3. Recon dumps: the streaming write — .partial file then atomic rename (a local-file idiom, but the same all-or-nothing visibility principle as S3 multipart-complete: readers only ever see complete objects).

The content-addressing idea (from your topic list, one paragraph): key objects by SHA256(content) → dedup for free, integrity check on read, immutability natural (same content = same key). Where yours could use it: recon dumps (dedupe identical capture bundles) and WAL segments (integrity verification at restore). Where it doesn't fit: user-visible keys (career applications need ownership/permissions keyed by identity, not content).

Zoth's object-storage absence is itself the answer: no blobs in the domain (policies are rows, documents are text-embedded). The resume-relevant sentence: "I know exactly what S3 would buy zoth — exported audit archives per tenant, lifecycle to Glacier for retention — and chose not to add it at demo scale."

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

Why are WAL segments a good fit for S3 while policies are not?

WAL segments are immutable, write-once, key-stable, read-rarely (only at restore) — blob semantics, and huge relative to row semantics. Policies are transactional state: constrained, joined, priority-ordered, updated — relational semantics. The rule is access pattern: blobs with identity keys → object storage; state with invariants → database. My PITR design puts each in its natural home, with separate buckets and credentials so the two blast radii never overlap.

02

What is the hot-partition problem in S3?

S3's partition map spreads key prefixes across storage nodes; sequential keys sharing a prefix (timestamps, '2026/09/…') concentrate on one node with a per-prefix throughput cap — your writes throttle. Fixes: prefix spreading (hash-suffixed keys, e.g. {hash}/{timestamp}), or batching into fewer, larger objects. My WAL segments are low-volume, so it's irrelevant — but it's why high-ingestion S3 pipelines (log shippers) hash their key prefixes.

03

When would you move traverse's resume files back into Postgres?

Almost never — but the honest boundary: small blobs (under ~100 KB, e.g. avatars) stored in Postgres bytea can be simpler to operate (one backup, one permission model, no pre-signed URL machinery) at the cost of TOAST bloat and buffer pressure. My resumes are multi-hundred-KB to MB user files with per-application ownership — clearly object-storage territory. The decision line is size × access pattern, and I'd state it in those terms rather than 'files always go to S3.'