FjellSystem Design Lab
Lesson 9.3• 9 min read

Storage: SQL or NoSQL, object storage and expiry

Decoupling metadata indexes from blob payloads, and designing lazy vs active expiration sweepers.

What you will learn

  • Keep small metadata rows in a database and the paste content in an object store.
  • Choose between SQL and NoSQL for a lookup by key.
  • Delete expired pastes, and answer reads of them correctly before the sweeper runs.

Metadata in a database, content in an object store

The primer separates the two:

  • A relational database acts "as a large hash table, mapping the generated url to a file server and path containing the paste file".
  • Rather than run a file server, use a managed object store such as Amazon S3, or a NoSQL document store, for the paste content.

The primer's table:

shortlink char(7) NOT NULL
expiration_length_in_minutes int NOT NULL
created_at datetime NOT NULL
paste_path varchar(255) NOT NULL
PRIMARY KEY(shortlink)

The primary key on shortlink creates an index that enforces uniqueness. The primer adds an index on created_at "to speed up lookups (log-time instead of scanning the entire table) and to keep the data in memory". It gives the reason: reading 1 MB sequentially from memory takes about 250 µs, from SSD 4 times longer and from disk 80 times longer.

Keeping content out of the rows keeps those rows small, so far more of the index fits in memory. The object store holds content cheaply and grows without resharding: the primer notes S3 "can comfortably handle" 12.7 GB of new content per month.

A Bit.ly-style shortener needs no object store at all. Its "content" is the long URL, which fits in the row.


SQL or NoSQL for the metadata

The access pattern is a lookup by primary key, which either kind of store handles well. The primer uses the relational approach, suggests a NoSQL key-value store as the alternative, and points to its SQL-or-NoSQL trade-offs (chapter 5).

At the primer's 4 writes per second, a single primary with replicas is enough. If writes grow, the primer lists federation, sharding, denormalization and SQL tuning, or moving some data to NoSQL. A key-value store can make sharding by shortlink easier. Some also offer built-in expiry; DynamoDB's TTL is one example (below).


Deleting expired pastes

The primer's approach: scan the database for entries whose expiration time has passed, then delete them or mark them expired. The primer's table stores a length in minutes. Storing the absolute time instead, as an expires_at column with an index, lets the scan touch only expired rows:

SELECT shortlink, paste_path FROM pastes
WHERE expires_at <= NOW()
LIMIT 5000;   -- delete in batches, then delete each object from the store

The sweeper runs periodically, so a paste can outlive its expiry until the next run. Every read must therefore check expiry itself. If the paste has expired, return an error and trigger its deletion right away.

For the error, RFC 9110 offers 410 Gone for a resource that is "intentionally unavailable", which it says is common "for limited-time, promotional services". A server that cannot tell whether the removal is permanent should send 404 Not Found instead.

Redis expires keys the same two ways. A key is removed "passively" when a client tries to access it after it has timed out. Because "there are expired keys that will never be accessed again", Redis also periodically tests random keys that have an expiry and deletes the expired ones.

Managed TTLs work the same way. DynamoDB "automatically deletes expired items within a few days of their expiration time, without consuming write throughput". Until then, expired items can still appear, so AWS advises filtering them out of query and scan results.

Metadata, blob storage and expiry

Test relational metadata indexes, S3 blob tiering, lazy deletions on read, and active background cron sweepers.

Clock: T+100s
Metadata DB rows
4 active
S3 blob volume
15.8 KB
Reclaimed disk
0.0 KB freed
New paste:
Stored pastes & expiration stateClick "Read" to trigger lazy deletion check
/cfg-nginxNginx reverse proxy config
Blob: blobs/cfg-nginx.txt (1420 B) • Created: T+60s
Expired
/sql-dumpDatabase migration schemaRedis
Blob: blobs/sql-dump.sql (8450 B) • Created: T+75s
Expires in 20s
/jwt-helperOAuth2 JWT signature verifierRedis
Blob: blobs/jwt-helper.ts (2100 B) • Created: T+95s
TTL: Never
/err-stackCrash dump stack trace
Blob: blobs/err-stack.log (4200 B) • Created: T+40s
Expired
Storage engine activity log
Storage tiering active: metadata in Relational DB, text payloads in S3 Object Store.
Figure 3: Tiered storage architecture separating relational metadata indexes from unstructured S3 blob content.
  • API Compute Cluster: 4 × App Compute Server
  • Metadata Primary DB (MySQL / DynamoDB): 2 × Primary Relational DB
  • Object Storage S3 (Paste Blobs): 2 × Object Storage (S3)
  • Redis Hot Metadata Cache: 2 × Redis In-Memory Cache
  • Active TTL Sweeper Cron Worker: 2 × App Compute Server
  • Expired Deletion Event Queue: 2 × Message Queue / Kafka
  • API Compute Cluster sends requests to Metadata Primary DB (MySQL / DynamoDB)
  • API Compute Cluster sends requests to Object Storage S3 (Paste Blobs)
  • API Compute Cluster sends requests to Redis Hot Metadata Cache
  • Active TTL Sweeper Cron Worker sends requests to Metadata Primary DB (MySQL / DynamoDB)
  • Active TTL Sweeper Cron Worker sends requests to Expired Deletion Event Queue
  • Expired Deletion Event Queue sends requests to Object Storage S3 (Paste Blobs)

Key takeaways

  • Small rows keep the index in memory; an object store holds the content cheaply and grows without resharding. A Bit.ly-style shortener needs no object store.
  • A lookup by primary key suits either kind of store; at 4 writes a second one primary with replicas is enough.
  • A periodic sweeper leaves expired pastes behind for a while, so every read must check expiry itself and answer 410 Gone (or 404).
  • Redis deletes an expired key when it is read and in a periodic background sweep; DynamoDB deletes expired items in the background within a few days, so filter them out of reads.

Sources

Adapted from The System Design Primer by Donne Martin, licensed under CC BY 4.0. Changes were made.