Full Stack Engineering Day 3
Database Design Masterclass
Full Stack Engineering · Day 3

Design data that stays correct, useful and fast.

A scrollable masterclass on relational modelling, transactions, backend integration, NoSQL awareness, time-series data and semantic vector search.

Requirements → ERDJava + PostgreSQLTransactions & ACIDORM + Connection PoolVector Search
01

Start with the workload, not the product.

Different databases optimize different jobs. A full-stack engineer should identify correctness rules, access patterns and growth before selecting technology.

Database landscape connecting relational, document NoSQL, key-value, time-series, vector and graph databases to a full-stack application

Relational

Structured business data, relationships, constraints and multi-step transactions.

NoSQL

Document, key-value, wide-column and graph models for particular access patterns.

Vector & time series

Semantic similarity on one side; timestamped, append-heavy measurements on the other.

WorkloadExample recordNatural fit
Order9001 · customer 42 · paid · ₹79,999Relational
Sessionsession:abc123 → customer 42 · expires 10:30Key-value
API metric10:15:30 · checkout · 184 msTime series
Semantic representationproduct-101 → vector + metadataVector
Pragmatic default: keep one clearly identified system of record. Add specialized stores only when the workload earns the complexity.
02

Translate business language into a schema.

Seven-step database design journey from business requirements to indexes and performance

ShopSphere requirements

  • Customers maintain addresses.
  • Products belong to categories.
  • Orders contain products and reserve inventory.
  • Payment and stock must not partially update.
  • Natural-language product search is required.

Questions before tables

  1. What must survive?
  2. What rules must always be true?
  3. What are the important reads and writes?
  4. Which steps belong together?
  5. How will volume grow?

Conceptual → logical → physical

1

Conceptual model: business entities and relationships, independent of a database product.

2

Logical model: tables, columns, primary keys, foreign keys and normalization.

3

Physical model: PostgreSQL types, indexes, partitions and storage decisions.

Write down invariants

An invariant is a condition that must remain true after every valid transaction.

  • Product price cannot be negative.
  • Inventory cannot fall below zero.
  • An order item must reference a real order and product.
  • A paid order must have an associated payment record.

Constraints protect structural invariants; transaction logic protects multi-step business invariants.

CUSTOMER
  • PK customer_id
  • email UNIQUE
  • full_name
1 places N
ORDER
  • PK order_id
  • FK customer_id
  • status
  • total_amount
1 contains N
ORDER_ITEM
  • PK order_id + product_id
  • quantity
  • unit_price
order_id · PKcustomer_id · FKstatustotal
900142paid₹79,999
900242shipped₹1,499
900343pending₹4,299

Rows 9001 and 9002 point to the same customer without duplicating that customer's email or name.

03

Read every relationship in both directions.

“One customer places many orders” also means “each order belongs to one customer.” The second sentence reveals where the foreign key belongs.

One-to-many · Customer → Orders

CUSTOMER
  • PK customer_id
1 : N
ORDER
  • PK order_id
  • FK customer_id

Rule: put the parent key on the many side.

One-to-one · Customer → Profile

CUSTOMER
  • PK customer_id
1 : 0..1
PROFILE
  • PK FK customer_id

Rule: shared primary key or a UNIQUE foreign key.

Many-to-many · Products ↔ Categories

PRODUCT
  • PK product_id
1 : N
PRODUCT_CATEGORY
  • PK FK product_id
  • PK FK category_id
N : 1
CATEGORY
  • PK category_id

Rule: resolve N:M through a junction table.

Optional · Order → Coupon

ORDER
  • PK order_id
  • FK coupon_id NULL
0..1
COUPON
  • PK coupon_id

Rule: nullable foreign key when absence is valid.

SQL · Junction table
CREATE TABLE product_categories (
  product_id  BIGINT REFERENCES products(product_id),
  category_id BIGINT REFERENCES categories(category_id),
  assigned_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (product_id, category_id)
);
product_idcategory_idMeaning
10110TravelBook belongs to Laptops
10120TravelBook belongs to Travel Technology
20510GamePro belongs to Laptops

The composite primary key prevents product 101 from being assigned to category 10 twice.

Delete behaviour is part of design. Use CASCADE for disposable children, RESTRICT for protected history, and SET NULL only for optional links.

Five-question relationship test

  1. How many B records can one A have?
  2. How many A records can one B have?
  3. Is either side optional?
  1. Does the relationship carry its own data?
  2. What should happen when either side is deleted?

Example: Enrollment is not merely a connector between Student and Course when it also stores enrollment date, status, completion and score. It is a business entity.

04

Give every fact one dependable home.

Normalization before and after showing one messy table transformed into customers, orders, order items and products

1NF

No repeating product columns. Store one order-item fact per row.

2NF

Every non-key fact depends on the entire key.

3NF

Customer facts belong to customers—not to orders.

PostgreSQL · Constraints protect every writer
CREATE TABLE products (
  product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  sku        VARCHAR(50) NOT NULL UNIQUE,
  name       VARCHAR(200) NOT NULL,
  price      NUMERIC(12,2) NOT NULL CHECK (price >= 0),
  status     VARCHAR(20) NOT NULL CHECK
             (status IN ('draft', 'active', 'discontinued'))
);
Attempted product rowResultRule
101 · LAP-14 · ₹79,999 · activeAcceptedAll constraints satisfied
102 · LAP-14 · ₹69,999 · activeRejectedDuplicate UNIQUE sku
103 · TAB-10 · ₹-500 · activeRejectedCHECK price ≥ 0
104 · NULL · ₹25,000 · activeRejectedNOT NULL sku
Normalized tableExample rowFact represented
customers42 · anita@example.com · Anita RaoWho the customer is
orders9001 · customer 42 · paidWho placed the order
order_items9001 · product 101 · quantity 1What was purchased
order_items9001 · product 205 · quantity 2Another item in the same order
05

Choose precise types. Express changes safely with SQL.

A data type defines which values a column accepts and which operations are valid. SQL defines structures, changes rows, reads results and controls transactions.

Core PostgreSQL data types

CategoryCommon typeColumn exampleExample value
Whole numberINTEGER / BIGINTquantity INTEGER3
Exact decimalNUMERIC(12,2)price79999.00
Short textVARCHAR(n)sku VARCHAR(50)LAP-14
Long textTEXTdescriptionLightweight laptop…
BooleanBOOLEANis_activeTRUE
Calendar dateDATEdate_of_birth1992-04-18
Real-world instantTIMESTAMPTZordered_at2026-08-19 10:15+05:30
Flexible structureJSONBspecifications{"ramGb":16}
Semantic representationVECTOR(n)embedding[0.12,-0.44,…]

Money

Use Java BigDecimal and SQL NUMERIC. Binary floating point can introduce rounding artifacts.

Time

Use DATE for a calendar date and TIMESTAMPTZ for an instant viewed across time zones.

NULL

NULL means missing or unknown. It is different from zero and an empty string. Test it with IS NULL.

SQL command families

FamilyPurposeCommands
DDLDefine structuresCREATE, ALTER, DROP
DMLCreate, change and remove rowsINSERT, UPDATE, DELETE
DQLRead rowsSELECT
TCLControl transactionsBEGIN, COMMIT, ROLLBACK
DCLControl permissionsGRANT, REVOKE

CRUD in one view

Create · INSERT
INSERT INTO products (sku, name, price, status)
VALUES ('MOU-6', 'Wireless Mouse', 749.50, 'active');
Read · SELECT
SELECT product_id, name, price
FROM products
WHERE status = 'active'
ORDER BY price ASC
LIMIT 20;
Update · UPDATE
UPDATE products
SET price = 699.00
WHERE product_id = 102;
Delete · DELETE
DELETE FROM products
WHERE product_id = 103
  AND status = 'draft';
Always verify the WHERE clause before UPDATE or DELETE. Without it, every row may be changed or removed.

Filtering and NULL handling

SQL · Predicates
SELECT product_id, name, price
FROM products
WHERE status IN ('active', 'draft')
  AND price BETWEEN 500 AND 100000
  AND description IS NOT NULL;

Types of SQL joins

A join evaluates an ON condition to combine related rows. The join type decides what happens to rows that do not match.

Six-panel diagram comparing INNER, LEFT, RIGHT, FULL OUTER, CROSS and SELF joins using employee and department rows

employees

employee_idnamedepartment_id
1Asha10
2Bilal10
3Chen20
4DivyaNULL

departments

department_iddepartment_name
10Sales
20Engineering
30Finance
JoinRows retainedUse it to answer
INNERMatches onlyWhich employees have a department?
LEFTAll left + matchesShow every employee, including unassigned people.
RIGHTAll right + matchesShow every department, including empty ones.
FULL OUTERAll rows from bothShow every employee and department.
CROSSEvery left × right pairGenerate every possible combination.
SELFMatches inside one tableWho manages each employee?

INNER JOIN · matches only

SQL
SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d
  ON d.department_id = e.department_id;

Result: Asha–Sales, Bilal–Sales, Chen–Engineering. Divya and Finance are omitted.

LEFT JOIN · keep the left

SQL
SELECT e.name, d.department_name
FROM employees e
LEFT JOIN departments d
  ON d.department_id = e.department_id;

Result: the three matches plus Divya–NULL.

RIGHT JOIN · keep the right

SQL
SELECT e.name, d.department_name
FROM employees e
RIGHT JOIN departments d
  ON d.department_id = e.department_id;

Result: the three matches plus NULL–Finance. Often written as a LEFT JOIN with table order reversed.

FULL OUTER · keep both

SQL
SELECT e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d
  ON d.department_id = e.department_id;

Result: all matches plus Divya–NULL and NULL–Finance. Support varies by SQL dialect.

CROSS JOIN · every combination

SQL
SELECT e.name, d.department_name
FROM employees e
CROSS JOIN departments d;

Result: 4 × 3 = 12 rows. A missing join condition can cause this row explosion accidentally.

SELF JOIN · employee → manager

SQL
SELECT e.name AS employee,
       m.name AS manager
FROM employees e
LEFT JOIN employees m
  ON m.employee_id = e.manager_id;

Result: Asha–NULL, Bilal–Asha, Chen–Asha, Divya–Bilal.

Reading rule: start with the left table. Must unmatched left rows remain? Must unmatched right rows remain? Those two decisions reveal the join type.

Aggregation

GROUP BY
SELECT status,
       COUNT(*) AS product_count,
       AVG(price) AS average_price
FROM products
GROUP BY status
ORDER BY status;
statusproduct_countaverage_price
active240374.25
draft124999.00
Logical query order: FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
06

How backend code reaches the database.

The controller handles HTTP. The service owns business rules and the transaction. The repository performs data access. The pool manages scarce connections.

ControllerHTTP
Servicebusiness + transaction
Repositorydata access
ORM / JDBCmapping + SQL
Driverprotocol
Poolconnections
PostgreSQLtruth

JDBC driver

Authenticates, sends SQL and parameters, converts types, returns rows and reports database errors.

ORM

Maps Java objects and relationships to rows. It reduces CRUD boilerplate but does not remove the need to understand SQL.

Connection pool

Reuses a bounded set of connections. Calling close normally returns a borrowed connection to the pool.

HTTP Request
Borrow Connection
Run Transaction
Return Connection
Java · JPA entity
@Entity
@Table(name = "products")
public class Product {
  @Id @GeneratedValue
  private Long productId;

  @Column(nullable = false, unique = true)
  private String sku;

  private BigDecimal price;
}
Spring Boot · HikariCP
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/shopsphere
    username: app_user
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 3000
An ORM is above JDBC; JDBC uses the database driver; both normally borrow connections from the same pool.

Connection pool state

ConnectionStateBorrowed byTransaction
C1In usePOST /ordersTX-501
C2In useGET /productsRead query
C3Available
C4Available
C5In useBackground jobTX-502
App instancesPool per instancePossible DB connections
11010
51050
1020200
Failure pointWhat the backend should doLikely signal
Validation failsReject before borrowing a connection400 Bad Request
Inventory update affects zero rowsThrow a business exception and roll back409 Conflict
Foreign-key constraint failsRoll back and map safely400 or 404, by API design
Pool is exhaustedWait only until configured timeout; alert503 or controlled 500
Database is unavailableFail fast, avoid retry storms, expose health signal503 Service Unavailable
ORM pitfalls worth demonstrating
  • N+1 queries: loading a list and lazily fetching one relationship per row.
  • Accidental eager graphs: one entity request loads far more data than intended.
  • Long persistence contexts: too many tracked objects increase memory and surprise updates.
  • Missing transaction boundary: multiple repository calls commit independently.
  • Assuming annotations replace constraints: the database still needs authoritative keys and checks.
07

One business action. One safe transaction.

ACID properties explained through an e-commerce order

Before transaction

TableVisible state
inventoryproduct 101 · available 1
ordersNo order 9006
order_itemsNo row for 9006

After commit

TableVisible state
inventoryproduct 101 · available 0
orders9006 · customer 42 · pending
order_items9006 · product 101 · quantity 1
A

Atomicity

Order, stock and payment record succeed together—or roll back.

C

Consistency

Constraints and rules remain valid. Inventory cannot go negative.

I

Isolation

Two buyers cannot both purchase the final unit.

D

Durability

A committed order survives a system failure.

Java · JDBC transaction
try (Connection connection = dataSource.getConnection()) {
  connection.setAutoCommit(false);
  try {
    // Atomic reservation: check and reduce together
    try (PreparedStatement ps = connection.prepareStatement("""
      UPDATE inventory
         SET quantity_available = quantity_available - ?
       WHERE product_id = ? AND quantity_available >= ?
      """)) {
      ps.setInt(1, quantity);
      ps.setLong(2, productId);
      ps.setInt(3, quantity);
      if (ps.executeUpdate() != 1)
        throw new InsufficientInventoryException();
    }
    insertOrder(connection, customerId, productId, quantity);
    connection.commit();
  } catch (Exception error) {
    connection.rollback();
    throw error;
  } finally {
    connection.setAutoCommit(true);
  }
}
Key idea: begin, every SQL statement, commit and rollback must use the same Connection.
Do not hold a database transaction open while waiting for a remote payment provider. Use short local transactions, idempotency and a reliable event/outbox workflow.
Transaction stepIf it fails before commit
Reserve inventoryNo order exists; inventory update is rolled back
Create orderInventory reservation is rolled back
Create order itemOrder and inventory changes are rolled back
CommitAll successful changes become durable together
08

Make important queries fast—deliberately.

Without an index

The database may inspect every order to find one customer's history.

Row 1
Row 2
Row N

With a composite index

The database navigates directly to one customer's newest orders.

customer_id
ordered_at DESC
Top 20
SQL · Index and query plan
CREATE INDEX idx_orders_customer_date
  ON orders (customer_id, ordered_at DESC);

EXPLAIN ANALYZE SELECT * FROM orders
 WHERE customer_id = 42
 ORDER BY ordered_at DESC LIMIT 20;
QueryUsefulness of (customer_id, ordered_at)Reason
customer 42 + newest firstExcellentMatches both columns
customer 42 onlyUsefulUses leading column
date onlyUsually poorSkips leading customer_id
status=paidNoneStatus is absent

Column order

Composite indexes follow query patterns. The leading column matters.

Write cost

Every index consumes storage and adds work to inserts and updates.

N+1

One list query plus one query per row is a backend access-pattern problem.

09

NoSQL is a family of models—not a shortcut.

Document model

Embed bounded data read together. Reference entities with independent lives or unbounded growth.

JSON · Product aggregate
{
  "id": "P101",
  "name": "TravelBook 14",
  "specifications": {
    "weightKg": 1.18, "ramGb": 16
  },
  "variants": ["silver", "blue"]
}

Time-series model

Timestamp + measurement + tags + fields. Designed for append-heavy metrics, telemetry and time-window aggregation.

Event · API latency
timestamp: 2026-08-19T10:15:30Z
measurement: api_request_duration
tags: service=checkout, region=ap-south-1
fields: duration_ms=184, status_code=201

Raw time-series rows

timeserviceregiondurationstatus
10:15:00checkoutap-south-1120 ms201
10:15:10checkoutap-south-1184 ms201
10:15:20checkoutap-south-1420 ms500

A five-minute downsample can turn these into one row: checkout · requests 3 · average 241.3 ms · errors 1.

If the dominant need is…Start by considering…Example
Correct multi-row business operationsRelationalOrders and payments
Flexible bounded aggregatesDocumentVariable catalogue specifications
Very fast key lookupKey-valueSessions and cache
Timestamp windows and retentionTime seriesAPI latency and IoT readings
Semantic similarityVectorNatural-language product search
10

Search by meaning, then confirm current truth.

Product text
Embedding modelnumbers capture meaning
Vector index
Query embedding
Natural-language query

Embedding

A numeric representation where semantically related inputs tend to be close.

Top-K retrieval

Return the nearest candidate IDs using cosine, dot-product or Euclidean distance.

Metadata filters

Restrict results by tenant, permission, status, category or price band.

PostgreSQL · pgvector
SELECT p.product_id, p.name, p.price,
       1 - (pe.embedding <=> :query_embedding) AS similarity
  FROM product_embeddings pe
  JOIN products p ON p.product_id = pe.product_id
 WHERE p.status = 'active' AND p.price <= 100000
 ORDER BY pe.embedding <=> :query_embedding
 LIMIT 10;

Vector lifecycle

  1. Read authoritative content.
  2. Build searchable text or chunks.
  3. Generate an embedding.
  4. Store vector, stable ID and metadata.
  5. Re-embed when source content changes.
  6. Delete vectors when access or source records disappear.

Failure modes

  • Stale embeddings after content updates
  • Mixed embedding-model versions
  • Missing tenant or permission filter
  • Poor chunks with insufficient context
  • Assuming similarity means factual correctness
  • No representative relevance evaluation
vector_idproduct_idvector previewstatusversion
product-101101[0.12, -0.44, 0.91, …]activeproduct-v3
product-205205[0.08, 0.52, -0.17, …]activeproduct-v3
product-310310[-0.33, 0.11, 0.62, …]discontinuedproduct-v2
11

Vector search finds meaning. RDBMS confirms truth.

Relational and vector databases working together for semantic product search
Natural-language query
Vector searchcandidate IDs
PostgreSQLprice, stock, permissions
Final results
Keep authoritative price, inventory, payment and permissions in the relational system of record. Treat vectors as search representations.

PostgreSQL · source of truth

productpricestockstatus
101 · TravelBook 14₹79,9996active

Vector index · search representation

vectorproducthashindexed
product-101101a81f…14:20:04

Outbox synchronization rows

outbox_ideventproductstatusattempts
771product.updated101processed1
772product.updated205pending0
773product.deleted310retry2
12

Apply the full design method.

Learning platform scenario

Learners enroll in courses, complete lessons, make payments, search lesson content semantically, and generate video-playback telemetry.

  • Identify entities and relationships.
  • Choose primary and foreign keys.
  • Add five integrity constraints.
  • Normalize to approximately 3NF.
  • Define the paid-enrollment transaction.
  • Propose two indexes.
  • Decide whether document NoSQL is justified.
  • Design a time-series playback event.
  • Define vector text and metadata.
  • Explain source-to-vector synchronization.
Reference solution

Core entities: User, Course, Module, Lesson, Enrollment, LessonProgress and Payment. Resolve users ↔ courses through Enrollment. Keep payment/enrollment changes transactional locally. Store playback telemetry by timestamp. Store lesson chunks and access-tier metadata in the vector index.

Reflection: If only one database could remain in this architecture, which would it be? The answer reveals the system of record.