Module 2 · Databases

Lesson 12 — NoSQL

Redis, MongoDB and search engines: what you sacrifice with each one and when it pays off.

Published
In this lesson
  1. Objectives
  2. 1. What you sacrifice with each store
  3. 2. Redis: the five uses that are worth it
  4. 3. JSONB: the NoSQL you already have
  5. 4. Anti-patterns you will see (and how to respond)
  6. Self-assessment

Stack: PostgreSQL + Redis · Project: TicketFlow Status: Published — closing the data module Prerequisite: Lesson 11 — Migrations


Objectives

  1. Choose between Postgres and a specialized store with judgement (what you sacrifice with each).
  2. Use Redis as cache, atomic counter, distributed lock and leaderboard — with sane TTLs.
  3. Use PostgreSQL's JSONB when the "NoSQL" you need fits inside your current DB.
  4. Recognize the anti-patterns: Mongo "because it's modern", Redis as the primary database.

1. What you sacrifice with each store

StoreGainsSacrificesIn TicketFlow
PostgreSQLACID, constraints, SQL, EXPLAINsimple writes at extreme scalethe source of truth (always)
Redisµs latency, atomics, TTL, structuresdurability (by default), no SQL, RAMcache, counters, locks, queues (broker)
MongoDBflexible documents, easy shardingjoins/multi-doc ACID (improved, but not its point)varied read-only catalogs (here: no)
Elasticsearchtext search, aggregationsconsistency, sync costevent search (later, if demand exists)

The senior rule: Postgres is the primary database; everything else is specialization justified by a metric (availability latency, counter throughput, text search). "I need NoSQL" is not a metric.

2. Redis: the five uses that are worth it

python
import redis
r = redis.Redis(host="localhost", port=6379, db=0)

# 1. Cache with TTL (availability, public listing)
r.setex(f"avail:{event_id}", 10, json.dumps(free_seats))   # TTL 10s: "fresher-ish" data

# 2. Atomic counter (per-IP rate limit, tickets per user)
r.incr(f"rate:{ip}:{minute}", ex=60)

# 3. Distributed lock (prevent double gift-card redemption across 2 pods)
lock = r.lock(f"gift:{card_code}", timeout=5, blocking_timeout=2)
with lock:                       # SET NX + expiry: the lock dies if the pod dies
    charge_the_ledger(...)

# 4. Leaderboard (top sales) — sorted sets
r.zincrby("sales:ranking", amount, event_id)
r.zrevrange("sales:ranking", 0, 9, withscores=True)

# 5. Simple queue (Celery's broker: Lesson 29)
r.lpush("jobs:emails", job_id)

What you must know: Redis is single-threaded (atomic commands: that is why INCR and SET NX solve concurrency without locks on your side), in-memory (durability depends on RDB/AOF config: it is not your source of truth), and SQL-less (key-based access patterns: if you need "all the X where condition", that's not Redis).

3. JSONB: the NoSQL you already have

Semi-structured fields with an index, inside Postgres:

sql
ALTER TABLE events_event ADD COLUMN metadata JSONB DEFAULT '{}';
CREATE INDEX idx_event_metadata ON events_event USING GIN (metadata);

SELECT * FROM events_event
WHERE metadata @> '{"venue_type": "stadium"}';   -- the GIN index serves it

When: attributes that vary per event (capacity config per zone, organizer preferences) without a schema that evolves through migrations. What you lose: per-field constraints, NOT NULLs, hard types. Rule: JSONB for decorations; columns for identity and money.

4. Anti-patterns you will see (and how to respond)

  • "Mongo because the schema changes fast" → the schema changes through versioned migrations precisely to control that change (Lesson 11). What changes daily is not the schema: it's the discipline.
  • "Redis as primary DB" → the queue loses messages if Redis dies (partial persistence), no ad hoc queries, everything must fit in RAM. Redis is a co-factor, not the protagonist.
  • "Polyglot everywhere" → every new store is: another deploy, another backup, another failure mode, another set of drivers. The cost is paid in operations (Module 9), not in the MVP.
  • Cache stampede → 1000 requests hit the DB when the hot cache expires. Antidote: TTL with jitter, or a regeneration lock (only one worker recomputes).

Self-assessment

  1. Which three questions do you ask before adding a new store to the stack?
  2. Why does Redis's INCR solve the rate limit without a race condition, while a Postgres SELECT + UPDATE (without FOR UPDATE) doesn't?
  3. What happens in production if the gift-card lock has no TTL and the pod dies holding it?
  4. When do you choose JSONB over real columns, and which constraint do you lose along the way?
  5. Your availability cache's hit rate is 40% with a 10s TTL. What do you look at: the TTL, the key, or that the query shouldn't be cached at all?

Continue with the exercises. The solutions only after trying it yourself.