What this topic is really about
When people start learning object-oriented design, they often ask:
- if I design classes, how do those become database tables?
- should I design objects first or database first?
- how should domain objects and schema influence each other?
- how do I avoid a bad mismatch between code and storage?
These are the right questions.
Because real systems usually need both:
- a good object model
- a good database schema
And those two are related, but they are not the same thing.
That is the first idea to understand clearly.
The key principle
Object-oriented design and database schema design solve different problems.
Object-oriented design focuses on:
- responsibilities
- behavior
- invariants
- collaboration
- clean boundaries in code
Database schema design focuses on:
- persistence
- storage structure
- integrity constraints
- querying
- updates
- efficiency
So:
- OOD asks: “How should the software think?”
- schema design asks: “How should the data live?”
Those questions overlap, but they are not identical.
If you confuse them, you usually get one of two bad results:
- the code becomes shaped entirely by tables
- the database becomes shaped entirely by class structure
Both can go wrong.
Good design comes from understanding where they align and where they differ.
The wrong extremes
Extreme 1: database-first everything
In this approach, people start with:
- tables
- columns
- foreign keys
- joins
Then they build code that mirrors the schema directly.
This often leads to:
- weak domain models
- anemic objects
- business logic spread across services
- classes that are just wrappers over rows
This is common in CRUD-heavy systems.
It can work for simple apps, but it often becomes painful when business logic grows.
Extreme 2: object-first without persistence thinking
In this approach, people design a beautiful object model and ignore storage realities.
Then later they discover:
- some relationships are hard to persist
- queries are expensive
- transactions are unclear
- aggregates load too much data
- performance becomes awkward
This also fails in practice.
The correct approach
You should usually design with both in mind, but not at the same level at the same time.
That means:
- first understand the domain and object responsibilities
- then shape persistence to support that model
- then refine the object model and schema together where needed
This is an iterative design activity, not a one-shot conversion.
A practical way to think about the relationship
Think of the system in three layers:
- domain model
- persistence model
- database schema
These are related, but they are not always identical.
1. Domain model
This is your object-oriented design view.
It includes:
- entities
- value objects
- behaviors
- invariants
- responsibilities
Example:
OrderOrderItemCustomerPayment
2. Persistence model
This is how the application saves and retrieves domain state.
It includes:
- repositories
- mappings
- loading rules
- transactions
- persistence-oriented shaping
3. Database schema
This is the actual storage structure.
It includes:
- tables
- columns
- foreign keys
- indexes
- unique constraints
- normalization choices
The biggest mistake beginners make is treating all three as if they must be identical.
They do not.
Start from the domain, not from random tables
When building from scratch, your first question should usually be:
What problem domain am I modeling?
Not:
What tables do I need?
That means you begin with:
- use cases
- business concepts
- rules
- state transitions
- invariants
Example for an ordering system:
- customer places order
- order has items
- order can be paid
- order can be cancelled before shipment
- product stock matters
From there you identify domain concepts:
CustomerOrderOrderItemProductPayment
Only after that should you think:
- how do these persist well?
This order matters because schema should support the domain, not replace it.
But do not ignore the database too long
Starting from the domain does not mean ignoring persistence.
You should bring in schema thinking early enough to ask:
- what must be stored permanently?
- what relationships must be queryable?
- what constraints belong in the database too?
- what data volume or access patterns matter?
So the process is not:
- finish all OOD
- then suddenly think about the database
The process is:
- start from domain understanding
- then continuously check whether persistence can support it cleanly
That balance is what actual designers do.
The main alignment: concepts often map, but not perfectly
In many systems, domain concepts roughly map to database tables.
Examples:
Customer→customersOrder→ordersProduct→products
This is normal and useful.
But the mapping is not always one-to-one.
Sometimes:
- one object maps to multiple tables
- multiple objects use one table
- a relationship becomes a separate table
- a computed object is not stored directly at all
So use this rule:
Start by expecting alignment, then refine based on persistence and domain needs.
Understand the difference between object identity and row identity
In OOD, an object has identity as part of the domain.
Example:
- an
Orderis a meaningful business object
In a database, identity often appears as:
- primary key
- unique key
- foreign key references
These often align, but think carefully:
Domain identity questions
- what makes this thing distinct in the business?
- is the identity natural or generated?
- do users recognize this identity?
Examples:
OrderNumberEmailISBN- internal surrogate
id
Schema identity questions
- what should be the primary key?
- what should be unique?
- what should be indexed?
Sometimes the database uses:
- a generated surrogate key for joins
- plus a business key for domain meaning
That is very common.
Example:
orders.idas internal primary keyorders.order_numberas unique business identifier
This is a good example of OOD and schema solving related but different concerns.
Entities and tables
An entity in OOD usually represents a thing with stable identity and lifecycle.
Examples:
CustomerOrderReservationInvoice
These often map naturally to tables.
Example:
Order
- domain meaning: a committed purchase
- schema likely needs:
orderstable
Typical table columns might include:
idorder_numbercustomer_idstatuscreated_attotal_amount
This is a straightforward alignment.
But notice:
- the object also has behavior
- the table does not
That is one of the central differences:
objects carry behavior; tables carry stored state
Value objects and how they map
A value object in OOD is defined by its value, not identity.
Examples:
MoneyAddressDateRangeEmailAddress
These do not always become separate tables.
Common mapping options:
Option 1: inline into parent table
Example:
Addressfields stored inordersshipping_line1shipping_cityshipping_postal_code
This works when:
- the value object is small
- it belongs only to one parent
- independent querying is limited
Option 2: separate table
Example:
addressestable linked tocustomers
This works when:
- the value object is reused
- it has its own lifecycle concerns
- it is queried independently
Design rule
Do not ask:
- “Is
Addressan object or a table?”
Ask:
- “Is
Addressa domain concept?” - “Does it need independent persistence identity?”
- “How is it used and queried?”
That produces better designs.
Relationships in OOD and relationships in schema
Object-oriented design models relationships conceptually.
Database schema models them structurally.
The two often correspond, but in different forms.
One-to-many
OOD:
Customerhas manyOrders
Schema:
orders.customer_idforeign key
This is usually simple.
Many-to-many
OOD:
Studentrelates toCourse
Schema:
- join table such as
enrollments
This is important because in real systems many-to-many often becomes:
- not just a join
- but a meaningful domain concept
Example:
EnrollmentMembershipBookingOrderItem
This is one of the strongest bridges between OOD and schema design:
if a relationship has real data or rules, model it as a class and a table
Example:
Enrollmentstudent_idcourse_idenrolled_ongradestatus
That is both:
- a domain object
- a schema table
Composition and ownership in persistence
In OOD, some objects strongly belong to others.
Example:
OrdercontainsOrderItem
In schema design, this often means:
order_itemshas foreign key toorders- child rows usually do not exist without parent row
Example:
orders
idcustomer_idstatus
order_items
idorder_idproduct_idquantityunit_price
This is where object ownership and relational ownership line up well.
But remember:
- composition in code is conceptual and lifecycle-based
- foreign keys in schema are structural and integrity-based
They are related, not identical.
Behavior does not map to tables
This is a very important distinction.
Example object behavior:
order.cancel()loan.close()reservation.confirm()
There is no direct table equivalent of these methods.
What the database stores is the resulting state:
status = 'CANCELLED'returned_on = ...confirmed_at = ...
This means:
schema captures persistent facts; OOD captures the rules that move facts from one valid state to another
That is why you should not derive the object model from columns alone.
Invariants belong in both places, but differently
One of the best practical insights is this:
Important rules should often be enforced in both:
- application/domain layer
- database layer
But the form of enforcement differs.
In OOD
You enforce rules through:
- methods
- encapsulation
- validation logic
- workflow coordination
Example:
order.cancel()refuses cancellation after shipment
In schema
You enforce rules through:
NOT NULLUNIQUECHECK- foreign keys
- transactional constraints
Example:
customer_idmust not be nullorder_numbermust be uniquequantity > 0
Practical rule
Put business rule enforcement in domain code where behavior happens.
Put data integrity enforcement in the database where corruption must be prevented.
Use both.
Do not rely on only one.
Normalization vs object convenience
Database schemas often aim for normalization:
- avoid duplication
- improve consistency
- reduce update anomalies
Object models often aim for:
- expressive domain structure
- convenient behavior grouping
- understandable boundaries
These goals usually align, but not always perfectly.
Example:
- a fully normalized schema may spread related data across many tables
- but a domain object may want to act as one cohesive concept
This creates practical tension.
You should not force the object model to look exactly like join structure if that harms clarity.
And you should not denormalize recklessly just to make one class simpler.
The real skill is deciding where to keep conceptual purity and where to optimize for persistence reality.
Aggregates: the most useful bridge concept
If you want one concept that strongly connects OOD and persistence thinking, it is this:
aggregate
An aggregate is a cluster of related domain objects treated as a consistency boundary.
Typical examples:
Orderwith itsOrderItemsCartwith itsCartItemsInvoicewith its line items
The aggregate root is the main entry point.
Example:
Orderis the rootOrderItembelongs inside it
Why this matters for database design:
- aggregate boundaries often influence transaction boundaries
- they influence how data is loaded and saved
- they influence table ownership patterns
Example thinking:
- save order and its items together
- enforce item consistency through the order
- do not let random code mutate order items independently without going through order rules
This is one of the cleanest ways to make OOD and persistence work together.
A practical design flow from scratch
This is the process you can actually use.
Step 1: Understand the domain and use cases
Start with:
- who uses the system?
- what are the main actions?
- what business rules matter?
- what state changes happen?
Example for hotel booking:
- guest searches room
- guest books room
- booking has dates
- payment may be partial or full
- room availability matters
Do not open with tables yet.
Open with behavior and business meaning.
Step 2: Identify domain concepts
Extract concepts like:
GuestRoomReservationPayment
Then ask:
- which are entities?
- which are value objects?
- which are services?
Example:
Reservationis an entityDateRangemay be a value objectPaymentGatewayis an interface/service boundary
This gives you the object-oriented foundation.
Step 3: Identify responsibilities and invariants
Ask:
- what does each object know?
- what can each object do?
- what must always remain valid?
Example:
Reservation
- has check-in and check-out dates
- can confirm or cancel
- must always have valid date range
Room
- has type and availability-related facts
Now you are designing objects, not rows.
Step 4: Sketch the object relationships
Ask:
- who owns what?
- which relationships are many-to-many?
- which relationships carry data of their own?
Example:
- guest makes reservation
- reservation is for room
- payment belongs to reservation
This is where class-diagram-level thinking helps.
Step 5: Decide what must persist
Now transition into schema thinking.
Ask:
- what data must survive process restart?
- what must be queryable later?
- what must support reporting or auditing?
Example:
- reservation record must persist
- payment record must persist
- room details must persist
- transient pricing calculation object may not
Not every object becomes a stored table.
That is normal.
Step 6: Map entities and meaningful relationship objects to tables
Usually start with:
- one entity → one table
- one meaningful relationship object → one table
Example:
Objects:
GuestRoomReservationPayment
Schema:
guestsroomsreservationspayments
If a relationship itself has dates, status, or price, it likely deserves a table.
Example:
Reservationconnects guest and room and carries business data
That is exactly the kind of thing that should be both:
- an object
- a table
Step 7: Map value objects intentionally
Now ask for each value object:
- inline or separate table?
Example:
Money
- usually stored as columns such as amount and currency
Address
- may be embedded into
customers - or stored separately if reused and managed independently
Do not decide this mechanically.
Use:
- lifecycle
- reuse
- querying
- normalization concerns
as the basis.
Step 8: Add schema constraints that protect integrity
Now think like a schema designer.
For each table ask:
- what is required?
- what must be unique?
- what references what?
- what values are invalid?
Example for reservations:
guest_idnot nullroom_idnot nullcheck_out_date > check_in_date- indexes on room/date queries if search matters
This is where database discipline strengthens the design.
Step 9: Revisit object design based on persistence pressure
Now ask:
- is this object too large to load routinely?
- does this relationship cause awkward persistence?
- am I mixing unrelated concerns in one aggregate?
- does a reporting need suggest a different read model?
This is where refinement happens.
Sometimes you will split:
- one big object into smaller aggregates
Sometimes you will simplify:
- an overly pure object model that persists poorly
This is normal. Good design is iterative.
Example 1: E-commerce order system from scratch
Let us walk through this carefully.
Requirements:
- customer browses products
- customer adds items to cart
- customer places order
- payment is recorded
- order has status changes
Object-oriented design view
Core concepts:
CustomerProductCartCartItemOrderOrderItemPayment
Responsibilities:
Cart
- add items
- remove items
- compute subtotal
Order
- represent committed purchase
- own ordered items
- enforce lifecycle rules
Payment
- represent payment attempt or settlement
Schema design view
Possible tables:
customersproductscartscart_itemsordersorder_itemspayments
How they align
This aligns quite naturally:
- entity objects map to tables
- owned child objects map to child tables
- payment maps to its own table
Important design insight
OrderItem should exist in both models.
Why?
Because it is not just a join.
It has meaningful data:
- quantity
- purchase price
- maybe discount snapshot
That means:
- in OOD it is a real domain concept
- in schema it is a real table
Another important insight
Cart and Order should usually be separate concepts, even if some fields overlap.
Why?
Because they have different responsibilities and lifecycle meanings.
You should not collapse concepts just because the schema looks superficially similar.
Example 2: Library system from scratch
Requirements:
- library has books and copies
- member borrows a copy
- due dates matter
- reservations may exist
OOD view
Core concepts:
BookBookCopyMemberLoanReservation
Why both Book and BookCopy?
Because:
Bookis the abstract titleBookCopyis the physical borrowable unit
That is a domain distinction first.
Schema view
Possible tables:
booksbook_copiesmembersloansreservations
Strong alignment
This is a case where the domain distinction directly improves schema quality.
If you only had books, you would struggle to represent:
- multiple copies
- copy-specific availability
- copy-specific loss or damage
So here the object model helps the schema become more correct.
Relationship object insight
Loan should not be just a hidden association.
It needs:
member_idbook_copy_idloan_datedue_datereturned_on
That means:
- it is a proper object in OOD
- it is a proper table in schema
Example 3: School enrollment system from scratch
Requirements:
- students take courses
- enrollment has semester, status, and grade
OOD view
Naive object thinking might say:
Studenthas manyCourseCoursehas manyStudent
But that is incomplete.
Because enrollment has its own business meaning.
So the stronger model is:
StudentCourseEnrollment
Schema view
Tables:
studentscoursesenrollments
Important columns in enrollments:
student_idcourse_idsemesterstatusgrade
Key lesson
Whenever the relationship has its own attributes or rules:
- make it explicit in OOD
- make it explicit in schema
This is one of the highest-value rules in both worlds.
When the schema should influence the object design
Beginners sometimes hear “design domain first” and assume the schema should never influence objects.
That is wrong.
Schema concerns should influence OOD when they reveal real system constraints.
Examples:
1. Data volume
If an object graph is huge, loading it as one conceptual unit may be a bad idea.
Example:
Customerwith millions of orders should not behave like everything lives in one in-memory object cluster
That may push you to:
- narrower aggregates
- query-specific loading
- separate read models
2. Transaction boundaries
If consistency must be maintained together, that influences aggregate design.
Example:
OrderandOrderItemoften save together- order and product catalog usually do not
3. Query patterns
If the application constantly asks:
- all orders by status for date range
- all reservations for room on a date
then schema and indexing must support that, and the persistence model may need dedicated query pathways.
4. Reporting and audit
Sometimes what must be stored is shaped by history and traceability.
Example:
- order item stores purchase-time unit price, not just current product price
That is both:
- a schema decision
- a domain correctness decision
When the object model should resist the schema
Sometimes raw schema structure should not leak directly into the object model.
Examples:
1. Join-table thinking that hides business meaning
If the database has a join table but the relationship is business-significant, expose it as a domain object.
Example:
Enrollment- not just
student.courses
2. Denormalized reporting tables
A reporting table may exist for performance, but it should not automatically become the core domain model.
3. Technical persistence fields
Fields like:
created_atupdated_at- internal version columns
may exist in schema but should not dominate domain design unless they have domain meaning.
The domain model should reflect business meaning first.
Repositories: where OOD and schema often meet
A repository is often the bridge between:
- object-oriented domain model
- database persistence
Example:
OrderRepositoryCustomerRepository
A repository is responsible for:
- loading domain objects
- saving domain objects
- hiding query/storage details from domain logic
This is useful because it keeps:
- the domain focused on behavior
- the persistence layer focused on schema and storage mechanics
Important:
A repository should represent meaningful aggregate access, not every random SQL idea.
Examples:
findOrderByIdsaveOrder
Less ideal:
- giant repository interfaces that mirror every possible table query without domain meaning
Transactions: the practical glue
OOD and schema truly meet at transactions.
A transaction determines:
- what must succeed together
- what must remain consistent together
Example checkout:
- create order
- create order items
- create payment record
Which of these must happen atomically?
That depends on the design, but the question is essential.
This is why persistence cannot be an afterthought in OOD.
You must ask:
- what are my consistency boundaries?
- what changes together?
These answers often shape:
- aggregate design
- repository design
- schema foreign key structure
A practical checklist when doing both from scratch
Use this checklist.
Domain checklist
Ask:
- what are the main business concepts?
- which are entities, value objects, and services?
- what invariants matter?
- which relationships are meaningful business concepts?
- which object owns each rule?
Schema checklist
Ask:
- what must persist?
- what becomes a table?
- what becomes columns inside another table?
- what relationships need foreign keys?
- what needs uniqueness constraints?
- what indexes are needed for real queries?
Alignment checklist
Ask:
- where does one object map cleanly to one table?
- where does a relationship deserve both object and table status?
- where is the object model more expressive than the schema?
- where does persistence reality require design refinement?
- what must load and save together?
If you think through all three checklists, your design will be much stronger.
Common mistakes
1. Treating ORM classes as the whole design
An ORM mapping is not the full object model and not the full schema design.
It is only one layer of translation.
2. Mirroring tables directly into anemic classes
This often produces weak OOD.
3. Ignoring schema constraints because “the app validates it”
This is fragile. Use database integrity features too.
4. Ignoring domain meaning because “the table already works”
A working table structure can still support a poor software design.
5. Modeling every value object as a separate table
That can create unnecessary complexity.
6. Failing to make relationship objects explicit
This is one of the most common design misses.
Examples:
EnrollmentOrderItemReservationMembership
The relationship in one statement
Object-oriented design models the domain in terms of responsibilities, behavior, and invariants. Database schema design models how persistent data is structured, constrained, and queried. They usually align around the same domain concepts, but they are not identical. A good design starts from business concepts and behaviors, then maps entities and meaningful relationships into tables, uses schema constraints to protect integrity, and refines both object and persistence models together based on transaction boundaries, query needs, and data volume.
Bottom line
When designing from scratch:
- start from domain understanding
- build the object model around responsibilities and invariants
- then shape the schema to persist the important state correctly
- use tables for entities and meaningful relationship records
- use database constraints for integrity
- use repositories and transactions to connect the two worlds
- refine both models together instead of forcing one to perfectly mirror the other
The goal is not:
- “make classes equal tables”
The goal is:
- “make domain design and persistence design support each other without collapsing into the same thing”
That is how object-oriented design and database schema design go together in real systems.