Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Relational Databases Deep Dive

The foundation of structured data storage

A relational database organizes data into tables (relations) with rows (tuples) and columns (attributes). Tables are connected through relationships defined by keys.

Diagram

ACID is an acronym for four essential properties that guarantee reliable database transactions:

Diagram

Atomicity means a transaction is all-or-nothing. Either all operations succeed, or all fail (rollback).

Diagram

Example: Transferring money between accounts. If deducting from Account A succeeds but adding to Account B fails, both operations are rolled back. You can’t have money disappear!


Consistency ensures the database always remains in a valid state. All constraints, rules, and relationships are maintained.

Diagram

Example: If a table has a constraint “age must be > 0”, the database will reject any transaction that tries to insert a negative age, keeping the database consistent.


Isolation ensures concurrent transactions don’t interfere with each other. Each transaction sees a consistent snapshot of the data.

Diagram

Example: Two users updating the same account balance simultaneously. Isolation ensures each transaction sees a consistent view and updates don’t interfere.


Durability guarantees that once a transaction is committed, it persists even if the system crashes.

Diagram

Example: After you commit a bank transfer, even if the server crashes immediately after, your transaction is safely stored and will be restored when the system recovers.


Normalization is the process of organizing data to reduce redundancy and eliminate anomalies. It involves splitting tables and creating relationships.

Diagram

Normalization follows normal forms (NF), each eliminating specific types of redundancy:

Diagram

Before Normalization (Problems):

order_idcustomer_namecustomer_emailproduct_namepricequantity
1Alice[email protected]Laptop10001
2Alice[email protected]Mouse202
3Bob[email protected]Keyboard501

Problems:

  • ❌ Customer info repeated (redundancy)
  • ❌ If Alice changes email, need to update multiple rows
  • ❌ If we delete order #2, we lose Alice’s info

After Normalization (3NF):

Customers Table:

customer_idnameemail
1Alice[email protected]
2Bob[email protected]

Orders Table:

order_idcustomer_idproduct_namepricequantity
11Laptop10001
21Mouse202
32Keyboard501

Benefits:

  • ✅ Customer info stored once
  • ✅ Update email in one place
  • ✅ Delete order without losing customer info

An index is a data structure that speeds up data retrieval. It’s like a book’s index—instead of scanning every page, you look up the index to find the exact location.

Diagram

Analogy: Finding a word in a dictionary:

  • Without index: Read every page (full table scan)
  • With index: Use alphabetical index to jump directly (index lookup)

Diagram
AspectBenefitCost
Read Speed✅ Much faster queries❌ Slower writes (must update index)
Storage-❌ Extra storage space
Maintenance-❌ Index must be maintained

How relational database concepts affect your class design:

Entity Class Mapping to Table
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass
class User:
"""Maps to 'users' table"""
id: Optional[int] = None # Primary key
name: str
email: str # Has unique index
created_at: datetime = datetime.now()
@dataclass
class Order:
"""Maps to 'orders' table"""
id: Optional[int] = None # Primary key
user_id: int # Foreign key -> users.id
total: float
created_at: datetime = datetime.now()
# Relationship (not in DB, but in code)
user: Optional[User] = None
Repository Pattern
from abc import ABC, abstractmethod
from typing import List, Optional
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: int) -> Optional[User]:
"""Find user by primary key"""
pass
@abstractmethod
def find_by_email(self, email: str) -> Optional[User]:
"""Find user by email (uses index)"""
pass
@abstractmethod
def save(self, user: User) -> User:
"""Save user (handles insert/update)"""
pass
class SQLUserRepository(UserRepository):
def __init__(self, db_connection):
self.db = db_connection
def find_by_email(self, email: str) -> Optional[User]:
# Uses email index for fast lookup
query = "SELECT * FROM users WHERE email = %s"
# Execute query...
ACID Transaction
class AccountService:
def __init__(self, db_connection):
self.db = db_connection
def transfer(self, from_account_id: int, to_account_id: int, amount: float):
# Atomicity: All or nothing
with self.db.transaction(): # BEGIN TRANSACTION
try:
# Deduct from source
self.db.execute(
"UPDATE accounts SET balance = balance - %s WHERE id = %s",
(amount, from_account_id)
)
# Add to destination
self.db.execute(
"UPDATE accounts SET balance = balance + %s WHERE id = %s",
(amount, to_account_id)
)
# Consistency: Check constraints
if amount < 0:
raise ValueError("Amount must be positive")
# Commit (Durability: persisted)
self.db.commit() # COMMIT
except Exception:
# Rollback on any error (Atomicity)
self.db.rollback() # ROLLBACK
raise


Now that you understand relational databases, let’s explore how isolation levels control what concurrent transactions can see:

Next up: Database Isolation Levels — Learn how read committed, repeatable read, and serializable isolation levels prevent race conditions.