Back to Blog
Lesson 9 of the System Design: System Design Fundamentals course
ArchitectureJuly 26, 20264 min read

Data Modeling for Scalable Systems: Normalization and Performance

Master data modeling for scalable systems. Learn to normalize schemas for integrity and denormalize for performance to meet your application's unique needs.

data modelingdatabasesqlschemaperformancesystem design
Close-up of cooling fans in a server room, showcasing technology and efficiency.

Previously in this course, we covered ACID properties in relational databases to ensure transactional integrity. Now that you understand how to protect data during operations, we will focus on data modeling—the art of organizing that data so your system remains both maintainable and performant as it grows.

Normalizing Your Database Schema

Normalization is the process of structuring a database to reduce redundancy and improve data integrity. We typically aim for Third Normal Form (3NF) in most relational systems.

The core principle is simple: One fact, one place. If you store a user's address in both the Orders table and the Users table, updating that address becomes a nightmare—you risk "update anomalies" where the order shows an old address but the profile shows a new one.

To reach 3NF, follow these steps:

  1. 1NF (Atomicity): Ensure each column contains only atomic (indivisible) values. No comma-separated lists in a single field.
  2. 2NF (No Partial Dependencies): Ensure all non-key columns depend on the entire primary key.
  3. 3NF (No Transitive Dependencies): Ensure non-key columns depend only on the primary key, not on other non-key columns.

Mapping Entities to Tables

When you translate your business requirements into a schema, start by identifying the "nouns." These become your tables.

Consider a simple E-commerce project. We have Customers, Products, and Orders.

SQL
-- The Normalized approach (3NF)
CREATE TABLE customers (
    customer_id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);

CREATE TABLE products (
    product_id UUID PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2)
);

CREATE TABLE orders (
    order_id UUID PRIMARY KEY,
    customer_id UUID REFERENCES customers(customer_id),
    order_date TIMESTAMP
);

By keeping these separate, we ensure that changing a product's price doesn't require updating every historical order record.

Denormalizing for Performance

While 3NF is great for write-heavy consistency, it often forces "expensive" JOIN operations. In a high-traffic system, joining five tables to display a user's order history might cause unacceptable latency.

Denormalization is the intentional introduction of redundancy to speed up reads. You sacrifice write-time consistency for read-time speed.

Common Denormalization Patterns:

  • Pre-computing aggregates: Adding a total_spent column to the customers table instead of calculating SUM(order_price) on the fly.
  • Caching frequently accessed data: Storing the customer_name directly in the orders table to avoid joining with the customers table during list views.

Worked Example: The Performance Trade-off

Imagine our orders table is hit by thousands of requests per second. Joining it to products to show the product name is slow. We decide to denormalize:

SQL
-- Denormalized approach
CREATE TABLE orders (
    order_id UUID PRIMARY KEY,
    customer_id UUID,
    product_name VARCHAR(255), -- Redundant: now stored in both places
    price_at_purchase DECIMAL(10, 2) -- Snapshot: protects against future price hikes
);

By storing product_name and price_at_purchase directly in the orders table, we serve the request with a single SELECT from one table. The cost? If the product name changes, we don't update the old orders (which is actually a feature here, as it preserves history).

Hands-on Exercise

Review your running project design document. Identify one "read-heavy" query (e.g., fetching a user profile with their recent activity). Create a schema snippet that normalizes the data, then write a short paragraph explaining which column you would duplicate (denormalize) to improve performance, and why.

Common Pitfalls

  • Premature Denormalization: Don't denormalize before you have evidence of a performance bottleneck. It makes your application code significantly more complex because you must manually keep the redundant data in sync.
  • Ignoring Data Types: Using strings for IDs or overly large types (like TEXT where VARCHAR(50) would do) hurts index performance. Always map your entities to the most efficient types.
  • Over-normalization: Sometimes, you can go too far. If a relationship is always accessed together and never queried separately, keeping it in one table might be more efficient than splitting it into two.

FAQ

  • When should I start denormalizing? Only when your latency metrics (as we'll discuss in measuring system latency) show that JOIN operations are the primary bottleneck.
  • Does normalization hurt write performance? Yes, because multiple tables might need to be updated. However, the integrity benefits usually outweigh the costs unless you are dealing with extreme high-concurrency writes.

Recap

We started by defining entities to create a clean, normalized schema. We then explored when to break those rules through denormalization to optimize for read-heavy system requirements. Balancing these two approaches is the core of effective data modeling for scalable systems.

Up next: Implementing Schema Migrations

Similar Posts