06. Class and Schema Design — draw the floor plan before writing walls¶
~12 min read. If the drawing is fuzzy, the code and tables wobble.
Built on the ELI5 in 00-eli5.md. The floor plan — classes and links before code — connects object design to table design.
1) Class diagrams tell you where each responsibility should live¶
A class diagram is pre-code thinking, not decoration. See. You are deciding which room owns which facts and which rules. That choice decides how expensive future change becomes. Take a tiny checkout flow. A customer buys 2 notebooks at ₹120 each and 1 pen at ₹35. Subtotal becomes ₹275. Those numbers must sit in the right class.
┌────────────────┐ 1 * ┌────────────────┐
│ Cart │───────────────▶│ CartItem │
├────────────────┤ ├────────────────┤
│ cartId │ │ productId │
│ customerId │ │ quantity │
│ items[] │ │ unitPricePaise │
│ subtotal() │ │ lineTotal() │
└────────────────┘ └────────────────┘
class CheckoutManager {
List<CartItem> items;
void applyCoupon(String code) { }
void chargeCard() { }
void saveOrder() { }
void sendConfirmation() { }
}
class Cart {
private final List<CartItem> items;
int subtotalPaise() { return 27500; }
}
class PricingService {
int discountPaise(Cart cart, String couponCode) { return 5000; }
}
class OrderRepository {
void save(Order order) { }
}
Cart carries facts, PricingService applies policy, and OrderRepository stores aggregates.
That is a useful floor plan.
Quick checklist:
- Entity: identity and lifecycle matter.
- Value object: small immutable bundle like Money.
- Service: coordinates rules across classes.
- Repository: loads and saves aggregates.
2) Relationships and cardinality are meaning, not just arrows¶
Many learners memorize arrow names and stop there. That is not enough. Relationships tell you ownership, lifetime, and coupling. Cardinality tells you scale.
Association: ┌──────────┐────────────▶┌──────────┐
│ Teacher │ teaches │ Course │
└──────────┘ └──────────┘
Aggregation: ┌──────────┐◇───────────▶┌──────────┐
│ Team │ │ Player │
└──────────┘ └──────────┘
Composition: ┌──────────┐◆───────────▶┌──────────┐
│ Order │ │ Item │
└──────────┘ └──────────┘
Inheritance: ┌──────────────┐
│ PaymentMethod│
└──────▲───────┘
│
┌─────────┴─────────┐
│ │
┌────────────┐ ┌───────────┐
│ CardPay │ │ UpiPay │
└────────────┘ └───────────┘
class Order {
private final List<OrderItem> items;
}
interface TaxCalculator {
int taxPaise(Order order);
}
class CheckoutService {
private final TaxCalculator taxCalculator;
}
OrderItem is composition. TaxCalculator is composition-friendly behavior injection.
Why not subclass CheckoutService for every tax rule? Because the hallway should stay stable while policy changes.
Now add multiplicity.
┌──────────────┐ 1 * ┌──────────────┐
│ Customer │─────────▶│ Subscription │
└──────────────┘ └──────┬───────┘
│ 1
▼ *
┌──────────────┐
│ Invoice │
└──────────────┘
Suppose one customer can hold 3 subscriptions, and each subscription creates 12 invoices yearly.
With 8,000 customers, you can reach 24,000 subscriptions and 288,000 invoices.
See how a tiny 1:* note starts affecting storage, pagination, and indexing?
Many-to-many also deserves honesty.
┌──────────────┐ 1 * ┌──────────────┐ * 1 ┌──────────────┐
│ Cart │────────▶│ CartItem │◀────────│ Product │
└──────────────┘ └──────────────┘ └──────────────┘
CartItem is not just glue. It carries quantity, locked price, and discount snapshot.
Simple, no?
3) Derive database schema from class intent without losing meaning¶
Now move from objects to tables. Do it one rule at a time. Map identity, ownership, and multiplicity carefully. See.
Start with this class view.
A clean first schema can look like this.
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
total_paise BIGINT NOT NULL,
ship_city VARCHAR(80) NOT NULL,
ship_pin_code VARCHAR(10) NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
line_no INT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price_paise BIGINT NOT NULL,
PRIMARY KEY (order_id, line_no),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
Notice the signal. order_items uses the parent key plus line_no, so uniqueness lives inside one order.
No extra surrogate key is compulsory here.
Take a number example. If daily traffic creates 25,000 orders and each order averages 4 items, you store 100,000 order-item rows daily. In 180 days, that becomes 18 million rows. So what to do? Create indexes early for known access paths.
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
Value objects need one more decision.
Embed ShippingAddress when it belongs only to one order snapshot.
Normalize it when many aggregates genuinely reuse the same record.
Many-to-many becomes a join table, especially when the relation carries facts.
CREATE TABLE enrollments (
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
enrolled_at TIMESTAMP NOT NULL,
status VARCHAR(20) NOT NULL,
PRIMARY KEY (student_id, course_id)
);
The class model should preserve meaning, and the schema should preserve constraints. When both tell the same story, maintenance becomes calmer.
Where this lives in the wild¶
- A Swiggy backend engineer maps
Restaurant,MenuSection, andMenuItemso lunch-hour ordering stays consistent. - A Razorpay Java engineer designs
Payment,Refund, andLedgerEntryrelations so money movement remains auditable. - An Amazon catalog SDE decides when product variation needs inheritance, and when composition keeps the model safer.
- A PhonePe service engineer turns wallet classes into tables with unique keys that block duplicate debits.
- A Zoho CRM platform developer separates
Lead,Contact, andAccountso schema changes stay local.
Pause and recall¶
- When does composition communicate more truth than association?
- Why can a many-to-many relation deserve its own class?
- What does 1:N cardinality hint about indexing and pagination?
- When would you embed an address instead of normalizing it?
Interview Q&A¶
Why composition not association for Order and OrderItem?¶
Because the child has no meaningful life outside the parent order. Composition makes deletion rules and invariant boundaries explicit. Common wrong answer to avoid: “Both are just links, so either arrow is fine.”
Why composition not inheritance for Cart and CartItem?¶
Because an item is part of a cart, not a specialized cart. Inheritance would fake an “is-a” relation that simply does not exist. Common wrong answer to avoid: “Inheritance gives reuse, so it must be better.”
Why explicit Enrollment not direct many-to-many between Student and Course?¶
Because the relationship carries data like status and timestamp. An explicit class gives those facts names, validation, and lifecycle rules. Common wrong answer to avoid: “Join tables are only database details, not design details.”
Why embedded address not separate Address table for every case?¶
Because reuse and lifecycle decide the answer, not purity slogans. If an address belongs only to one order snapshot, embedding is simpler. Common wrong answer to avoid: “Normalization is always superior, so always split tables.”
Apply now (5 min)¶
Exercise. Take a movie ticket booking flow. List 6 nouns, convert 4 into classes, mark one association, one composition, and one 1:N relation. Then write the first three table names with primary keys.
Sketch from memory.
Draw Customer, Booking, SeatHold, and Show.
Add multiplicities beside every relationship, then explain why SeatHold should not be a primitive field list.
Bridge. The floor plan is clear. Next, define the hallway shapes other systems can trust. → 07-api-contract-design.md