>_
EngineeringNotes
Back to DBMS Topics
Lecture 19

Database Design Patterns & Scaling

Scaling a database is not just about upgrading servers. It requires choosing the right type of database, optimizing queries, implementing connection pools, and transitioning architectural models as workload scales. Let us explore this in a step-by-step case study.

1

Case Study: Cab Booking App (The Startup Phase)

Imagine you are building a new ride-hailing app, CabFlow. Let us look at how the architecture starts and scales over time.

Startup TierTiny Startup
Daily Audience~10 Customers Onboard
Ride Velocity~1 Booking in 5 Mins

Initial Database Choice: Single-Machine SQL

At this stage, all application data (customers, trips, real-time driver coordinates, and transaction history) is stored inside a single small machine database.

Recommended DB: Relational SQL (PostgreSQL or MySQL)
  • ACID Compliance: Crucial when matching drivers to passengers. Booking must be atomic (either the trip is booked or fails; a driver cannot be double-booked to two riders simultaneously).
  • Relationship Mapping: Tables like `Users`, `Drivers`, and `Trips` have direct relations that are easily handled via SQL `JOIN` statements.
  • Zero Overhead: Easy to configure, backup, and host on a single shared instance costing less than $10 a month.
2

Going Viral: The Problem Begins

Your application becomes famous and downloads spike! Now, you are processing ~30 bookings per minute.

What bottlenecks are we experiencing?

1. Sluggish App & High Latency

API response latency increases dramatically. Simple actions like logging in, searching for driver history, or displaying past rides take seconds. Customers start dropping off due to sluggishness.

2. Deadlock, Starvation & Transaction Aborts

Multiple client requests concurrently lock rows in the `Drivers` table to reserve them. Relational locks conflict, causing transactions to time out, face deadlocks, starve, and return frequent booking failures.

3

Pattern 1: Query Optimisation & Connection Pool Implementation

To address the 30 bookings/minute bottlenecks without spending money on bigger servers, we implement query optimization and caching.

Cache Non-Dynamic Data

Offload read operations by caching user profiles, ride histories, and payment logs in an in-memory cache (like Redis).

Redundancy & Indexing

Identify heavy slow-running SQL queries, add B-Tree indexes on search fields (like `DriverID` or `PassengerID`), and optimize joining logic.

Connection Pooling

Cache open database sockets so that threads do not waste database process CPU constantly handshaking TCP connections.

Pattern 1: Connection Pool Socket Reuse & In-Memory Redis Caching
App InstancesNode / Python API
Query / Cache
Connection Pool
Socket A (Active)Socket B (Idle)
Redis CacheReads profiles in <1ms
SQL DatabaseWrites bookings directly

Deep Dive: What is Connection Pooling & How to Achieve It?

Opening a physical database connection is an expensive operation. It involves initiating a TCP Handshake, authenticating credentials, and allocating a dedicated thread/memory space on the database server. If done for every single API call:

Request Flow WITHOUT Pool: Client request → App opens TCP connection → Authenticates with DB → Runs Query → Closes Connection. Total overhead: 15-50ms per query!

Connection Pooling solves this by establishing a collection (pool) of active connections on startup.

Request Flow WITH Pool: Client request → App borrows open connection from Pool → Runs Query → Returns connection immediately back to Pool. Total overhead: < 1ms!

How we achieve it:

By installing and configuring connection pooling drivers at the application layer (e.g. `pg-pool` or `Sequelize` for Node.js, `HikariCP` for Java, or `SQLAlchemy` for Python). We define parameters like:

  • minPoolSize: Minimum connections kept open in the background (e.g., 5).
  • maxPoolSize: Maximum connections the app can open during peak traffic (e.g., 20).
  • idleTimeout: How long an unused connection survives before being closed to free up database RAM.

CRITICAL BOTTLENECK: Connection Pooling vs. Horizontal App Scaling

A common pitfall occurs when developer teams scale their application servers horizontally (e.g. adding 15 NodeJS Docker containers behind a load balancer to handle web traffic) but leave the database server unscaled.

The Connection Exhaustion Scenario

Suppose each Node server has `maxPoolSize = 30`. If you have only 1 application instance, the database only experiences 30 active connection processes.
If you scale horizontally to 30 Application Server instances, each instance starts its own pool. The database is suddenly hammered with:
30 App instances × 30 connections = 900 active DB sockets!

Why it Crashes the Database

Each database connection consumes physical operating system processes/threads, socket descriptor limits, and buffer memory. At 900 connections, Postgres or MySQL will run out of RAM, hit the OS connection limit, context-switch processes constantly, lock up, and refuse incoming traffic.

🛠️ How to resolve this in production: Introduce a centralized Database Connection Proxy (like PgBouncer for PostgreSQL or AWS RDS Proxy). The proxy runs close to the database server, receives all 900 application connections, and multiplexes them onto a small, highly optimized pool of 20-30 physical database sockets.
🎉 Pattern 1 is a success! Business scales to one more city, yielding ~100 bookings per minute.
4

Pattern 2: Vertical Scaling or Scale-up

As the startup grows and launches in more locations, the request rate climbs to 300 bookings per minute. Caching and pooling are no longer enough. The database CPU hits 95%. It is time to implement Vertical Scaling.

Pattern 2: Upgrading Instance to Multi-Core CPU for Parallel Transactions
Single-Core Small DB Server
T1 (Running)T2 (Queued)T3 (Queued)

Single thread processing. Transactions 2 and 3 must wait, leading to deadlock, lock timeouts, and thread starvation.

Multi-Core Upgraded Server
Core 1: T1Core 2: T2Core 3: T3

Parallel core execution. Multiple transactions execute concurrently in microseconds, eliminating wait latency and deadlocks.

What is Vertical Scaling (Scale-up)?

Vertical Scaling is the process of upgrading the physical hardware capacity of the existing single database instance. Instead of changing any application code or routing logic, you simply migrate the database to a larger, more powerful machine:

Double the RAMAllocates more memory for query buffering, table scans, and index storage.
Upgrade CPUIncreases core count so the database can execute more concurrent locking transactions.
Faster SSD (IOPS)Improves write and read speeds for transaction logs and indexes on disk.

Why it is chosen first (Pros)

Vertical scaling is highly pocket-friendly and easy in the initial stages. Cloud platforms (like AWS or GCP) let you upgrade database sizes with a single click. There is no need to partition tables, write routing logic, or manage distributed queries.

Why it is not a permanent solution (Cons)

Hardware costs do not scale linearly. A machine with 128 cores is exponentially more expensive than an 8-core instance. Furthermore, you will eventually hit a hardware ceiling—there are physical limits to how much RAM or CPU a single physical motherboard can support.

5

Pattern 3: CQRS (Command Query Responsibility Segregation)

You decided to scale the business to 2 more cities. The scaled-up big machine is no longer able to handle the massive volume of read/write requests simultaneously.

Pattern 3: Read-Write Segregation & Blinking Asynchronous Propagation
Client AppCabFlow App TierMASTER (Primary)Writes Only (ACID)SLAVE AReads (Searches)SLAVE BReads (Searches)Writes (Bookings)ReadsSync Log Propagation (~4s delay)

Separating Read and Write physically

Instead of querying the same database instance for everything, we separate read and write operations physically:

Writes go to the Primary NodeAll ride bookings, payment transactions, and updates (Commands) target a single Primary (Master) database node.
Reads go to the 2 Replica NodesAll driver location searches, rider history queries, and profile views (Queries) target the read-replicas.

CQRS Scaling Problems

  • Replication Lag impact: The lag between the primary write and the replica update is visible to users. E.g., a customer books a ride, gets a confirmation, but refreshing the page shows "No active booking" for a few seconds.
  • Primary Node overload: As the app expands to more cities, the volume of write commands grows. A single primary machine is no longer able to keep up with the write requests.
6

Pattern 4: Multi Primary Replication

To solve the primary write bottleneck, you ask: "Why not distribute write requests to the replicas also?" E.g., let all database nodes write data.

Pattern 4: Multi-Primary Circular Replication Loop (All nodes are Master & Slave)
Node A (Write/Read)Circular Link
Node B (Write/Read)Circular Link
Node C (Write/Read)Circular Link
←→

The circular ring configuration

In a Multi-Primary configuration, all database nodes are logically connected in a circular ring. Every machine acts as both a primary and a replica:

  • Write to any node: The client can submit a write transaction (booking a ride) to Node A, B, or C.
  • Read from any node: Reads are directed to whichever node responds to the internal network broadcast first.

Multi-Primary scaling pain

As you scale to 5 more cities, the write throughput grows to ~50 req/s. The system enters deep pain again. Because all nodes accept writes, conflict resolution is a nightmare. If Passenger 1 matches with Driver A on Node 1, and Passenger 2 matches with the same Driver A on Node 2 at the exact same millisecond, the nodes must negotiate who won the write. This leads to massive sync latency, locks, and aborted bookings.

7

Pattern 5: Partitioning of Data by Functionality

To reduce conflicts and load, we analyze the schemas. We notice that driver location updates (telemetry) are clogging up the transaction tables.

Pattern 5: Splitting the Monolithic Schema by Functionality & Orchestration
Backend ServerStitches Data in Memory
Auth & Profiles DBPostgreSQL Node 1
Trips & Bookings DBPostgreSQL Node 2
Driver GPS Telemetry DBNoSQL Geo-Index Node

Separating tables into physical DB systems

Instead of putting all tables in one schema, we divide them by domain functionality onto separate physical machines:

Auth / Profile DBStores basic customer and driver details. Queries are simple and low-frequency.
Trips & Bookings DBHeavy transactional database demanding high ACID consistency.
Location Telemetry DBHandles driver GPS coordinates updating every 3 seconds. High write-frequency, low consistency requirement.

The Join Responsibility Trade-off

Because tables reside on physically separate servers, the database can no longer perform native SQL `JOIN` queries.The backend application layer must now take the responsibility to join results. The backend must call DB 1 for profiles, DB 2 for booking status, and stitch the logs together in application memory.

Now, your team plans to expand the business to other countries across the globe!

8

Pattern 6: Horizontal Scaling or Scale-out (Sharding)

To scale CabFlow across continents, functional partitioning is insufficient. We need a way to store billions of user records horizontally.

Pattern 6: Horizontal Sharding via Routing Proxy
Client RequestShard Key: `RiderID = 101`
SHARDING ROUTER PROXYCalculates `Hash(101) % 3`
Shard 1Users 1 - 33
Shard 2Users 34 - 66
Shard 3Users 67 - 99

Allocating 50 machines under one schema

Instead of one monster server, we allocate 50 smaller machines, all having the exact same schema. Each machine holds only a subset (shards) of the data rows.

Locality of DataData is sharded based on geographical keys. Users in India are routed to India shards, while users in France are routed to Europe shards.
Shard-Level ReplicasEach shard machine has its own local read replicas. If a shard fails, it fails over immediately without impacting other shards.

"No Pain, No Gain"

Sharding is notoriously hard to implement. It requires a custom routing layer, mapping keys cleanly, and handling complex shard key updates. However, it is the only way to scale the database infinitely as you expand globally.

9

Pattern 7: Data Centre Wise Partition

When operating globally, requests travelling across continents (e.g. India app connecting to US databases) face severe latency due to the speed of light.

Pattern 7: Geographically Segregated Global Data Centres
US East Data CentreServes Americas Users
← Geolocation Sync →
EU West Data CentreServes European Users

Distributing traffic across data centres

We deploy completely independent Data Centres across continents. All queries in Europe are directed to the Europe Data Centre, and Asian requests to the Asia Data Centre.

Cross-Data Centre Replication

Data centres replicate vital master tables asynchronously. If a catastrophic network failure shuts down Data Centre US, Data Centre EU has the backup data ready, ensuring disaster recovery and zero downtime.

IPO Ready!

By scaling from a single SQL instance to connection-pooled, horizontally-partitioned, and cross-datacenter replicated engines, CabFlow is fully equipped to handle global scale. Time to plan the IPO! 😉

10

Interactive Database Scaling Roadmap

Click through the steps of the scaling journey below to see the database topology change and review the architectural choices made at each step:

Active Architecture Stage

Step 1: Single SQL Instance

Database Engine Config:

Relational SQL (Postgres / MySQL)

Current Load Volume:

~1 ride booking / 5 mins

Scaling Bottleneck Encountered:

None yet. System is fast, lightweight, and extremely cheap to run.

Optimisation / Architectural Fix:

A single instance stores all customers, trips, and location histories. High ACID compliance ensures no double-booking errors.

Visual Database Topology:
Riders / Drivers
App Server
Single SQL DBACID Booking