PrepZone Logo
PrepZone

Relational Schema Design

ER modeling, keys, and foreign keys for VaultCommerce users, products, and orders.

Why this matters

  • A well-designed schema makes every downstream query simpler; a poor one forces application-layer joins and duplicate data fixes.
  • Primary and foreign keys are the contract between microservices sharing Postgres tables or event payloads.
  • ER modeling is a standard system-design interview step before discussing indexes or sharding.
  • VaultCommerce's order service, reporting pipeline, and admin dashboard all read the same tables — design once, consume everywhere.
Clustered (PK)
Table rows (sorted by PK)
Row 1id=1
Row 2id=2
Row 3id=3
Non-clustered (email)
email index→ row pointer
Table heapunordered rows
A clustered index sorts table rows by key. Non-clustered indexes point to row locations.

Entities and relationships

VaultCommerce's core commerce domain has clear entities:

Core entities

  • Customer — Registers, has one email, places many orders.
  • Product — Belongs to a category; appears on many order lines.
  • Order — Belongs to one customer; contains many line items.
  • Order item — Links one order to one product with quantity and unit price snapshot.

Cardinality drives table layout: one-to-many relationships get a foreign key on the "many" side. orders.customer_id points to customers.id.

Primary keys and surrogates

Every table needs a stable row identifier. VaultCommerce uses surrogate numeric keys generated by the database:

Java
CREATE TABLE customers (
    id         BIGSERIAL PRIMARY KEY,
    email      VARCHAR(255) NOT NULL UNIQUE,
    full_name  TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE orders (
    id          BIGSERIAL PRIMARY KEY,
    customer_id BIGINT NOT NULL REFERENCES customers(id),
    status      VARCHAR(32) NOT NULL DEFAULT 'PENDING',
    total_cents INTEGER NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

BIGSERIAL auto-increments; REFERENCES customers(id) enforces that every order belongs to a real customer. Natural keys like email work for lookup but change rarely — surrogate id values never change when a user updates their address.

Junction tables for many-to-many

Products and promotional tags have a many-to-many relationship. Neither side stores an array of IDs — a junction table holds pairs:

Java
CREATE TABLE product_tags (
    product_id BIGINT NOT NULL REFERENCES products(id),
    tag_id     BIGINT NOT NULL REFERENCES tags(id),
    PRIMARY KEY (product_id, tag_id)
);

Composite primary key (product_id, tag_id) prevents duplicate tag assignments.

Modeling money and snapshots

Never store prices as FLOAT. VaultCommerce uses integer cents and snapshots unit price on each order line because catalog prices change:

Java
CREATE TABLE order_items (
    id           BIGSERIAL PRIMARY KEY,
    order_id     BIGINT NOT NULL REFERENCES orders(id),
    product_id   BIGINT NOT NULL REFERENCES products(id),
    quantity     INTEGER NOT NULL CHECK (quantity > 0),
    unit_price_cents INTEGER NOT NULL  -- price at purchase time
);

Historical orders stay accurate even when today's products.price_cents differs.

Timestamps and soft deletes

Audit columns appear on every VaultCommerce table:

Java
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Soft deletes use deleted_at TIMESTAMPTZ instead of DELETE — support can restore accounts without losing order history. Application queries add WHERE deleted_at IS NULL.

From diagram to DDL

Design workflow at VaultCommerce:

Schema design steps

  1. List entities and attributes from product requirements.
  2. Draw relationships and cardinalities (1:N, M:N).
  3. Assign primary keys and foreign keys.
  4. Add constraints — NOT NULL, UNIQUE, CHECK.
  5. Review access patterns — will checkout query by customer_id? Plan indexes next.

Quick recall

Everything you need if you only revisit this box.

  • Tables represent entities; foreign keys encode one-to-many and many-to-many (via junction tables) relationships.
  • Surrogate BIGSERIAL primary keys stay stable when natural attributes change.
  • Store money as integer cents; snapshot prices on order lines.
  • Composite keys on junction tables prevent duplicate associations.
  • Consistent naming (customer_id) and audit columns (created_at) pay off at scale.

Test yourself

Answer these before moving on — recall is what makes it stick.