📚 Module: Unit 5 • Persistence & Datastores

Databases: SQL, NoSQL & Distributed Caching

A first-principles engineering textbook covering the entire enterprise persistence tier. Master relational algebra, PostgreSQL DDL/DML, B-Tree index traversal, ACID concurrency isolation levels, database normalization (1NF through BCNF), modern ORMs (Prisma, Sequelize, SQLAlchemy, Hibernate), NoSQL document modeling with MongoDB, and ultra-low latency in-memory caching with Redis.

🐘 Relational: PostgreSQL 16+ & MySQL 8.0
🍃 NoSQL: MongoDB 7.0+ (WiredTiger)
⚡ In-Memory: Redis 7.2+ Cache-Aside
🎯 Formula: 60% Theory • 20% Code • 20% Architecture
← Unit 04: Version Control & Git Backend & Cloud Track • Unit 05 of 12 Track Index Syllabus →

Relational Database Systems (RDBMS) & Schema Design

The relational database model was formalized by Dr. Edgar F. Codd at IBM in 1970 based on first-order predicate logic and mathematical set theory. In a relational database, data is represented mathematically as relations (tables), where each row is a tuple (record) and each column is an attribute governed by strict domain data types.

PostgreSQL versus MySQL Storage Architecture

While both systems implement the Structured Query Language (SQL), their internal storage engines differ fundamentally:

Primary Keys, Foreign Keys & Integrity Constraints

Relational schemas enforce strict data invariants at the hardware storage layer:

PostgreSQL DDL schema/001_ecommerce_init.sql
-- Enterprise PostgreSQL E-Commerce Production Schema
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
    user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT chk_email_format CHECK (email ~* '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$')
);

CREATE TABLE orders (
    order_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL,
    order_status VARCHAR(50) NOT NULL DEFAULT 'pending',
    total_amount NUMERIC(12, 2) NOT NULL CHECK (total_amount >= 0),
    shipping_address JSONB NOT NULL,
    placed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_orders_user FOREIGN KEY (user_id) 
        REFERENCES users(user_id) 
        ON DELETE RESTRICT 
        ON UPDATE CASCADE
);

CREATE TABLE order_items (
    order_item_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    order_id UUID NOT NULL,
    sku VARCHAR(100) NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0),
    CONSTRAINT fk_items_order FOREIGN KEY (order_id) 
        REFERENCES orders(order_id) 
        ON DELETE CASCADE
);

Data Manipulation, Relational Joins & Window Functions

SQL is a declarative language: the backend engineer specifies what data is desired, and the database query optimizer formulates the physical relational algebra execution plan.

Relational Join Topology

When queries span multiple entities, the relational engine matches tuples across foreign key boundaries:

Figure 5.1 — Relational Join Operations (Inner vs Left vs Full Outer)
[INNER JOIN: A INTERSECT B] [LEFT JOIN: ALL A + MATCHED B] [FULL OUTER JOIN: ALL A + ALL B] +-------+-------+ +-------+-------+ +-------+-------+ / | \ / #######|#######\ / #######|#######\ / A | B \ / ##### A|##### B \ / ##### A|##### B \ | |#MATCH#| | | #######|#MATCH#| | | #######|#MATCH#|###| \ | / \ ##### |##### / \ ##### |##### / \ | / \ #######|#######/ \ #######|#######/ +-------+-------+ +-------+-------+ +-------+-------+ Only intersecting rows All A; NULL for B if unmatched All records from both tables
PostgreSQL DML queries/revenue_analytics.sql
-- Complex Analytical Query: Joins, Aggregation & Window Partitioning
SELECT 
    u.user_id,
    u.email,
    COUNT(DISTINCT o.order_id) AS total_orders,
    COALESCE(SUM(oi.quantity * oi.unit_price), 0.00) AS lifetime_spend,
    -- Window Function: Calculate user rank within entire cohort
    DENSE_RANK() OVER (ORDER BY COALESCE(SUM(oi.quantity * oi.unit_price), 0) DESC) AS customer_rank
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND o.order_status = 'completed'
LEFT JOIN order_items oi ON o.order_id = oi.order_id
WHERE u.is_active = true
GROUP BY u.user_id, u.email
HAVING COUNT(DISTINCT o.order_id) >= 1
ORDER BY lifetime_spend DESC
LIMIT 50;

Database Indexing & Query Optimization

Without an index, querying a table containing 10,000,000 rows forces the database storage engine to perform a Sequential Scan (Table Scan), reading every disk block sequentially from storage into memory ($O(N)$ computational complexity).

B-Tree (Balanced Tree) Index Mechanics

The default index in PostgreSQL, MySQL, and Oracle is the B-Tree. A B-Tree is a self-balancing, multi-way search tree where:

  1. Root & Internal Nodes: Contain ordered separator keys and memory pointers directing search operations down the tree.
  2. Leaf Nodes: Reside at identical depth and form a doubly-linked list. Each leaf contains the indexed column key alongside a TID (Tuple Identifier / Row ID) indicating the physical disk block address of the complete table row.

Traversing a B-Tree executes in $O(\log N)$ time: finding a record in a 10,000,000-row table requires only 3 to 4 pointer hops!

Figure 5.2 — B-Tree Index Traversal ($O(\log N)$) vs Sequential Disk Table Scan ($O(N)$)
[ROOT NODE: Keys 100, 500] / | \ Keys < 100 / 100 <= Keys < 500 \ Keys >= 500 v v v [INTERNAL NODE] [INTERNAL NODE] [INTERNAL NODE] / | \ v v v [LEAF NODE 1] <---> [LEAF NODE 2] <---> [LEAF NODE 3] (Doubly-Linked List) | Key: 42 | | Key: 150 | | Key: 512 | | TID: [Block 14, Offset 2] ----------------------------------+ | v [PHYSICAL DISK DATA PAGE] +-----------------------+ | Row 42: "usr_shivan" | +-----------------------+

The Composite Index Leftmost Prefix Rule

If an index is created on multiple columns: CREATE INDEX idx_orders_user_status ON orders(user_id, order_status), the B-Tree sorts primary data by user_id first, and by order_status second.
• A query filtering by WHERE user_id = '...' will use the index.
• A query filtering by WHERE user_id = '...' AND order_status = '...' will use the index.
• A query filtering only by WHERE order_status = '...' cannot use the index, because the tree is sorted primarily by user_id!

ACID Transactions & Concurrency Isolation Levels

A database transaction is a logical unit of work that must satisfy the four foundational ACID guarantees:

ANSI SQL Concurrency Isolation Levels & Anomalies

Isolation Level Dirty Read Non-Repeatable Read Phantom Read Engine Overhead
Read Uncommitted Allowed Allowed Allowed Lowest (Zero concurrency safety)
Read Committed (Postgres Default) Prevented Allowed Allowed Low (Snapshots per statement)
Repeatable Read Prevented Prevented Prevented (Postgres MVCC) Medium (Snapshots per transaction)
Serializable Prevented Prevented Prevented High (Strict serial dependency tracking)
PostgreSQL Transaction transactions/bank_transfer.sql
-- ACID Financial Fund Transfer Transaction with Row-Level Locking
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 1. Acquire pessimistic row lock on source account
SELECT balance FROM accounts 
WHERE account_id = 'acc_source_01' 
FOR UPDATE;

-- 2. Deduct funds from source account
UPDATE accounts 
SET balance = balance - 500.00 
WHERE account_id = 'acc_source_01' AND balance >= 500.00;

-- 3. Credit destination account
UPDATE accounts 
SET balance = balance + 500.00 
WHERE account_id = 'acc_target_02';

-- 4. Record audit ledger entry
INSERT INTO audit_ledger (from_acc, to_acc, amount, transferred_at)
VALUES ('acc_source_01', 'acc_target_02', 500.00, CURRENT_TIMESTAMP);

-- Atomically flush to Write-Ahead Log (WAL) and release locks
COMMIT;

Database Normalization: 1NF, 2NF, 3NF & BCNF

Database normalization is a formal mathematical process of decomposing tables to minimize data redundancy and prevent insertion, update, and deletion anomalies.

The Progressive Normal Forms

  1. First Normal Form (1NF): All attributes must contain atomic (indivisible) values. No repeating groups or comma-separated lists within a single column. Every row must have a unique identifier (Primary Key).
  2. Second Normal Form (2NF): Must satisfy 1NF, and must eliminate Partial Dependencies: every non-key attribute must depend on the entire candidate key, not a partial subset of a composite key.
  3. Third Normal Form (3NF): Must satisfy 2NF, and must eliminate Transitive Dependencies: non-key attributes must not depend on other non-key attributes ($A \rightarrow B$ and $B \rightarrow C$). In simple terms: "Every attribute must depend on the key, the whole key, and nothing but the key, so help me Codd."
  4. Boyce-Codd Normal Form (BCNF): A stricter extension of 3NF. A relation is in BCNF if and only if for every functional dependency $X \rightarrow Y$, $X$ is a superkey.

Object-Relational Mapping (ORM) & The N+1 Problem

Application code models systems using object-oriented graphs, classes, and inheritance. Relational databases model systems using two-dimensional tabular relations and foreign keys. This architectural mismatch is known as the Object-Relational Impedance Mismatch.

Enterprise ORM Ecosystem

🚨 The Infamous N+1 Query Problem

The most common database performance killer in backend applications occurs when an ORM fetches $N$ records (e.g., 100 orders), and then issues $N$ separate subsequent SQL queries inside a loop to fetch the user associated with each order ($1 + 100 = 101$ network roundtrips!).
Solution: Always use Eager Loading (include: { user: true } in Prisma, or JOIN FETCH in Hibernate) to retrieve all data in a single joined SQL query.

Prisma Schema & TypeScript src/infrastructure/db/prisma-client.ts
// 1. Prisma Declarative Schema Definition
// model Order {
//   id        String      @id @default(uuid())
//   userId    String
//   user      User        @relation(fields: [userId], references: [id])
//   items     OrderItem[]
// }

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({
  log: ['query', 'warn', 'error']
});

// Eager Loading: Solves the N+1 problem in a single joined database query
export async function getOrderWithDetails(orderId: string) {
  return await prisma.order.findUnique({
    where: { id: orderId },
    include: {
      user: {
        select: { id: true, email: true, firstName: true }
      },
      items: true
    }
  });
}

NoSQL Architectures & Document Databases (MongoDB)

As internet applications expanded to support millions of concurrent users and unpredictable schema models, traditional relational engines faced horizontal scaling limits. NoSQL (Not Only SQL) systems trade strict relational constraints for horizontal partition tolerance and dynamic data flexibility.

The CAP Theorem & BASE Properties

Formulated by Eric Brewer, the CAP Theorem dictates that a distributed datastore can simultaneously guarantee at most two out of three properties:

NoSQL systems replace strict ACID guarantees with BASE:
• Basically Available: The cluster guarantees availability through replica failovers.
• Soft state: Data values may change over time without immediate user interaction due to background replication.
• Eventual consistency: Given no new writes, all replica nodes will eventually converge to identical data.

MongoDB Architecture: WiredTiger & BSON

MongoDB is a distributed document database. It stores records as BSON (Binary JSON) documents inside Collections. BSON extends JSON by adding support for explicit data types (Double, Int32, Int64, Decimal128, Date, BinData, ObjectId).

TypeScript (Mongoose ODM) src/infrastructure/nosql/product.schema.ts
import { Schema, model, Document } from 'mongoose';

export interface IProduct extends Document {
  sku: string;
  title: string;
  price: number;
  attributes: Record<string, unknown>;
  tags: string[];
}

const productSchema = new Schema<IProduct>({
  sku: { type: String, required: true, unique: true, index: true },
  title: { type: String, required: true },
  price: { type: Number, required: true, min: 0 },
  // Dynamic Polymorphic Attributes (Unstructured NoSQL Strength)
  attributes: { type: Map, of: Schema.Types.Mixed },
  tags: [{ type: String, index: true }]
}, {
  timestamps: true
});

export const ProductModel = model<IProduct>('Product', productSchema);

In-Memory Caching Strategies & Redis Architecture

Reading from NVMe solid-state storage incurs microsecond latencies; querying relational indexes under heavy load introduces contention. Redis (Remote Dictionary Server) is an in-memory key-value data structure store delivering sub-millisecond responses directly from server RAM.

The Cache-Aside (Lazy Loading) Architecture

The gold-standard caching design pattern in backend distributed systems follows four sequential steps:

  1. Application receives an incoming HTTP request for a resource.
  2. Application queries Redis:
    • Cache Hit: Redis returns the serialized JSON payload. The database is never touched!
    • Cache Miss: Redis returns null.
  3. On Cache Miss, application queries the persistent SQL/NoSQL database.
  4. Application writes the queried result into Redis with a Time-To-Live (TTL) expiration timestamp, and returns the response to the client.
Figure 5.3 — Cache-Aside Design Pattern with TTL Invalidation
Client HTTP Request | v +------------------+ 1. Check Cache (GET key) +-------------------+ | BACKEND API | =================================> | REDIS IN-MEMORY | | SERVICE | <================================= | CACHE (RAM) | +------------------+ Cache HIT: Return Data +-------------------+ | (< 1ms) | | 2. Cache MISS (Fall back to persistent DB) v +------------------+ 3. Execute Query +-------------------+ | QUERY DATABASE | =================================> | POSTGRESQL / | | | <================================= | MONGODB DISK | +------------------+ Return Database Record +-------------------+ | | 4. Populate Cache: SET key value EX 3600 (TTL) v +------------------+ | Write to Redis | ---------------------------------> [REDIS: Stored for 1 Hour] +------------------+ | v Return Response to Client
TypeScript src/infrastructure/cache/redis-cache.ts
import Redis from 'ioredis';

export const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: Number(process.env.REDIS_PORT) || 6379
});

// Reusable Cache-Aside Wrapper Function
export async function getOrSetCache<T>(
  key: string,
  ttlSeconds: number,
  fetchFn: () => Promise<T>
): Promise<T> {
  // 1. Attempt Cache Hit
  const cachedData = await redis.get(key);
  if (cachedData) {
    return JSON.parse(cachedData) as T;
  }

  // 2. Cache Miss: Execute heavy persistent datastore query
  const freshData = await fetchFn();

  // 3. Populate Redis with automatic TTL expiration
  await redis.set(key, JSON.stringify(freshData), 'EX', ttlSeconds);

  return freshData;
}

Architectural Decision Matrix: When to Use SQL versus NoSQL

Evaluation Dimension Relational Databases (SQL: Postgres / MySQL) Document NoSQL (MongoDB) In-Memory Stores (Redis)
Data Structure Structured relational tables with strict typed schemas. Semi-structured, polymorphic BSON documents. Key-Value pairs with specialized data structures.
Transaction Guarantees Full ACID compliance across multiple tables and rows. ACID per single document (Multi-document transactions exist with latency overhead). Single command atomicity; lightweight multi-key transactions via Lua.
Query Complexity Exceptional: Multi-table JOINs, nested subqueries, window functions. Strong in Aggregation pipelines; poor at deeply normalized joins. Ultra-simple: Key lookups, range queries on sorted sets.
Scaling Vector Predominantly Vertical (Bigger CPU/RAM); Read replicas for read scaling. Native Horizontal Sharding across distributed nodes. Clustered memory sharding; sub-millisecond caching tier.
Ideal Production Use Case Financial banking, billing, order management, ERP, CRM. Content management (CMS), catalogs, dynamic form engines, analytics. Session state, rate limiting tokens, high-speed leaderboard caching.

University & Industrial Examination Review

1. Explain how a B-Tree index improves search performance from $O(N)$ to $O(\log N)$. What is the Leftmost Prefix rule in composite indexing?
Answer: Without an index, the database engine must execute a sequential scan, inspecting every physical page block in the table ($O(N)$). A B-Tree organizes separator keys into an ordered, multi-way balanced tree structure. Traversing from the root through internal nodes down to leaf nodes narrows the search space by a factor of the node fanout at each step, locating row pointers in $O(\log N)$ disk page reads. In a composite index on (A, B), the tree is sorted primarily by column A, and secondarily by column B. A query can only leverage the index if it filters on column A (the leftmost prefix); filtering solely on column B renders the ordered tree unusable.
2. Deconstruct the four ANSI SQL transaction isolation levels. What concurrency anomalies does each prevent?
Answer: 1. Read Uncommitted: Allows dirty reads (reading uncommitted changes from concurrent transactions). 2. Read Committed: Prevents dirty reads; a query only sees data committed prior to statement execution. Allows non-repeatable reads (data read twice within a transaction yields different values). 3. Repeatable Read: Prevents dirty reads and non-repeatable reads by snapshotting data at transaction start. (In PostgreSQL, MVCC also prevents phantom reads). 4. Serializable: Strictest isolation; completely eliminates all anomalies (dirty reads, non-repeatable reads, phantom reads, write skew) by enforcing serial execution order.
3. Formulate the conditions of Third Normal Form (3NF). Why do architects intentionally denormalize schemas in analytical data warehouses?
Answer: A relation is in 3NF if it satisfies 2NF (no partial functional dependencies on composite candidate keys) and contains zero transitive dependencies (no non-key attribute depends on another non-key attribute). In high-throughput read-heavy analytical data warehouses (OLAP), strictly normalized 3NF schemas require complex, expensive multi-table joins across millions of records. Architects denormalize tables (e.g., Star Schema with flattened Dimension and Fact tables) to eliminate join overhead and maximize sequential read throughput.
4. What is the N+1 query problem in Object-Relational Mappers (ORMs)? How is it detected and resolved?
Answer: The N+1 problem occurs when an application executes 1 initial SQL query to fetch $N$ parent records (e.g., SELECT * FROM orders), and then lazily executes $N$ additional independent SQL queries inside a loop to fetch the child relationship for each parent (SELECT * FROM users WHERE id = ?). This results in $N+1$ total queries and severe network round-trip overhead. It is resolved using Eager Loading (include in Prisma, JOIN FETCH in Hibernate), which instructs the ORM to fetch parents and children in a single joined SQL query.
5. Detail the Cache-Aside pattern using Redis. What is a Cache Stampede (Thundering Herd), and how is it mitigated?
Answer: In Cache-Aside, the application attempts to read data from Redis first. On a cache hit, it returns the cached data immediately. On a cache miss, it reads from the persistent database, writes the result to Redis with a TTL, and returns it. A Cache Stampede occurs when an intensely queried cached key expires: thousands of concurrent HTTP requests simultaneously experience a cache miss and swamp the underlying database with identical queries, potentially crashing the database. It is mitigated using distributed locking (e.g., Redis Redlock) so only one worker queries the database while others wait, or via probabilistic early expiration (XFetch algorithm).