What object-oriented design is
Object-oriented design, usually called OOD, is a way of designing software by organizing it around:
- objects
- responsibilities
- collaboration
- boundaries
Instead of thinking first in terms of:
- one long procedure
- one giant database schema
- one central script
- one sequence of global transformations
OOD asks:
- what things exist in this system?
- what does each thing know?
- what is each thing responsible for?
- how should those things work together?
The goal is not “use classes everywhere.”
The real goal is to build systems that are:
- understandable
- changeable
- testable
- reusable where appropriate
- resistant to accidental complexity
The core philosophy
The philosophy of object-oriented design is:
- model a system as a set of cooperating objects
- give each object a clear responsibility
- keep data and behavior related
- let objects collaborate through well-defined interfaces
- control complexity by dividing the system into cohesive parts
In short:
OOD is about responsibility and collaboration, not just classes and inheritance.
That distinction matters a lot.
Many beginners think OOD means:
- create many classes
- use inheritance heavily
- turn every noun into an object
That is not good OOD.
Good OOD is about:
- choosing the right abstractions
- assigning behavior to the right places
- reducing coupling
- preserving clarity as the system grows
How OOD differs from other design styles
Understanding OOD becomes easier when you compare it with other styles.
1. Procedural design
Procedural design organizes software around:
- functions
- steps
- control flow
Typical thinking:
- first do A
- then do B
- then update C
This can work very well for:
- scripts
- linear workflows
- small utilities
- transformation-heavy tasks
But as systems grow, procedural designs often suffer when:
- state is shared too widely
- rules are spread across many functions
- many parts need to coordinate
OOD tries to manage that by attaching behavior closer to the objects that own the relevant data and rules.
2. Data-oriented or relational thinking
This style organizes around:
- records
- tables
- schemas
- data processing
This is powerful for:
- persistence
- analytics
- reporting
- batch workflows
But if you design the entire software system only from the database outward, you often get:
- anemic domain objects
- business rules scattered in services
- poor boundaries around behavior
OOD is stronger when the problem is rich in domain rules and interactions.
3. Functional design
Functional design emphasizes:
- pure functions
- immutability
- explicit inputs and outputs
- minimized shared state
This is extremely strong for:
- predictability
- correctness
- concurrency
- data transformation
OOD is not the opposite of functional thinking.
In good modern systems, people often combine both:
- object-oriented structure for system organization
- functional style inside methods or services
4. Component or service-oriented design
This style focuses on:
- modules
- services
- APIs
- deployment boundaries
This operates at a larger scale than object-oriented design.
OOD is often about the internal design within a service or component.
So these are not enemies. They operate at different levels.
When OOD is a strong fit
Object-oriented design is especially useful when:
- the system has many interacting concepts
- behavior depends on object state
- business rules are rich and evolving
- the software will grow over time
- maintainability matters more than quick scripting speed
Examples:
- commerce systems
- booking systems
- library systems
- payment flows
- ride sharing
- workflow engines
- issue tracking
OOD is less important when:
- the job is a short script
- the task is mainly data transformation
- the main challenge is mathematical computation
- the system is tiny and unlikely to grow
The right design style depends on the problem.
The building blocks of OOD
To design well, you need to understand the main components of object-oriented design.
These are the real building blocks:
- objects
- classes
- attributes
- methods
- interfaces
- responsibilities
- relationships
- abstractions
- boundaries
The last four matter more than most beginners expect.
Objects
An object is a thing in the software that has:
- identity
- state
- behavior
Example:
- a
Customer - an
Order - a
Reservation - a
Session
An object is not just a bag of fields.
It should represent something meaningful in the system and should own behavior related to its state.
Classes
A class is a blueprint for creating objects.
It defines:
- what data an object holds
- what operations it supports
- what invariants it should maintain
Example idea:
- many different
Orderobjects may exist - they are all created from the
Orderclass design
Important:
OOD is not about maximizing the number of classes.
A small number of well-designed classes is better than many shallow ones.
Attributes and state
Objects carry state through attributes.
Example:
- an
Ordermay have:- status
- items
- customer
- total
State matters because behavior often depends on it.
Example:
- an order can be cancelled only if it is not already shipped
That is where OOD becomes valuable:
- the rules live close to the object whose state they govern
Methods and behavior
Methods define what an object can do.
Examples:
order.addItem(product, qty)order.cancel()reservation.confirm()account.lock()
A method is not valuable just because it exists.
A method is valuable when it places behavior in the right place.
Bad OOD often looks like:
- objects with only getters and setters
- all real logic living elsewhere
That is called an anemic model, and it often defeats the purpose of OOD.
Interfaces
An interface defines a contract without committing to one implementation.
Example:
PaymentGatewayNotificationSenderPricingStrategy
Interfaces help because they:
- reduce coupling
- allow substitution
- improve testing
- separate what is needed from how it is done
OOD becomes much stronger when you design to contracts instead of concrete implementations where appropriate.
Responsibilities
Responsibility is one of the most important ideas in all of OOD.
A responsibility is something an object should know or do.
Examples:
Cartis responsible for holding selected itemsOrderis responsible for enforcing order rulesPaymentProcessoris responsible for coordinating payment flowInventoryServiceis responsible for stock availability checks
When responsibilities are assigned poorly, the design becomes hard to evolve.
So a central OOD question is:
Which object should do this?
That question appears constantly in practical design work.
Abstraction
Abstraction means showing the important part while hiding irrelevant detail.
Examples:
PaymentGateway.charge()hides the payment provider detailsRepository.save()hides storage detailsNotificationSender.send()hides delivery mechanics
Good abstraction helps you:
- reason at the correct level
- avoid leaking implementation detail
- swap implementations without rewriting everything
Bad abstraction is either:
- too vague to be useful
- too detailed to be stable
Relationships and collaboration
Objects rarely work alone.
OOD requires deciding:
- which objects know about each other
- who owns what
- who calls whom
- what should be direct and what should go through an interface
This is where collaboration design matters.
A system is not just a pile of classes.
It is a network of responsibilities and interactions.
Boundaries
A boundary is where one responsibility stops and another begins.
Examples:
- domain model vs persistence
- business rules vs UI
- payment coordination vs payment provider implementation
- order logic vs inventory logic
Good boundaries prevent everything from bleeding into everything else.
Without clear boundaries, code becomes tightly coupled and fragile.
The deeper principles behind good OOD
These are the principles that separate surface-level OOD from real OOD.
Encapsulation
Encapsulation means keeping data and the rules for that data together, while controlling outside access.
Example:
- instead of letting any part of the system set
order.statusfreely - provide methods like
ship(),cancel(),markPaid()
Why this matters:
- business rules stay enforced
- invalid states become harder to create
- the object protects its own integrity
Weak encapsulation often leads to:
- random mutation
- duplicated validation
- inconsistent state
Good OOD usually prefers:
- meaningful behaviors over public field manipulation
Cohesion
Cohesion means how strongly related the responsibilities inside one class are.
High cohesion means:
- a class has a focused purpose
- its methods and fields belong together
Low cohesion means:
- the class does many unrelated things
Example of low cohesion:
UserManagerthat handles login, billing, email templates, reporting, and exports
That is usually a design smell.
High cohesion makes a class easier to:
- understand
- test
- change
Coupling
Coupling means how strongly one part depends on another.
High coupling means:
- changes in one class ripple into many others
- testing becomes harder
- replacement becomes expensive
Low coupling means:
- classes depend on stable contracts
- parts can evolve more independently
OOD usually aims for:
- high cohesion
- low coupling
That pair is one of the best general design heuristics to keep in mind.
Separation of concerns
This means different concerns should live in different parts of the system.
Examples:
- UI should not own pricing rules
- repositories should not own business policy
- domain entities should not render HTML
- payment provider adapters should not decide order eligibility
Once concerns mix, design quality drops quickly.
Information hiding
Information hiding means a class should expose what others need, but hide unnecessary internals.
Example:
- callers may ask
cart.total() - callers should not need to know how discount calculation is internally structured
This keeps dependencies smaller and change safer.
Composition over inheritance
This is a classic object-oriented design guideline.
It means:
- prefer building behavior by combining objects
- rather than forcing everything into inheritance trees
Inheritance can be useful, but it is rigid.
Composition is often more flexible.
Example:
- instead of
PremiumDiscountOrderandFestivalDiscountOrder - use a
PricingStrategyobject
That allows behavior to vary without growing a fragile class hierarchy.
Design to interfaces, not implementations
This means depending on contracts where variation matters.
Example:
CheckoutServicedepends onPaymentGateway- not directly on
StripeGateway
This helps with:
- testing
- substitution
- future changes
Do not apply this mechanically everywhere. Use it where flexibility is valuable.
The problem with naive object-oriented design
Naive OOD often has one or more of these problems:
- classes are just nouns from requirements
- behavior is pushed into one giant service class
- entities become bags of getters and setters
- inheritance is overused
- everything knows about everything
- there is no clear domain boundary
This often produces code that is formally object-oriented but not well-designed.
So the real question is not:
“Did I use classes?”
It is:
“Did I assign responsibilities and boundaries well?”
A practical mental model for OOD
When designing a system, think in this order:
- understand the domain
- identify the important concepts
- identify the key behaviors and rules
- assign responsibilities
- design collaborations
- protect invariants
- define boundaries to external systems
- refine for extensibility only where needed
This sequence is much more useful than starting from class syntax.
How to design a system from scratch using OOD
This is the most important part.
If you can do this well, you can design many object-oriented systems competently.
Step 1: Understand the problem deeply
Before designing classes, clarify:
- who uses the system?
- what are the main use cases?
- what rules matter?
- what constraints exist?
- what can change later?
If the problem understanding is weak, the design will be weak.
In practice, this means asking good questions first.
Example for an online bookstore:
- can one order contain many items?
- can items be cancelled individually?
- are discounts fixed or pluggable?
- can payment fail after stock is reserved?
Good design begins with correct problem framing.
Step 2: Identify core domain concepts
Extract the important concepts from the problem.
Example bookstore concepts:
CustomerCartOrderOrderItemProductPaymentInventory
Do not include every technical thing immediately.
Start with the business concepts first.
Later you can add:
- repositories
- gateways
- controllers
- adapters
This distinction is important:
- domain concepts first
- infrastructure later
Step 3: Identify responsibilities, not just names
For each concept, ask:
- what should it know?
- what should it do?
- what rules should it enforce?
Example:
Cart
- holds selected items
- can add or remove items
- can compute subtotal
Order
- represents a committed purchase
- enforces order lifecycle rules
- owns purchased line items
PaymentProcessor
- coordinates charging
- interacts with payment gateway
The class name alone is not enough. Responsibility is the real design unit.
Step 4: Find invariants and rules
An invariant is something that must always be true.
Examples:
- order total cannot be negative
- a shipped order cannot be cancelled
- a reservation check-out date must be after check-in
- cart quantity must be at least one
OOD gets stronger when you explicitly identify invariants and place enforcement where it belongs.
This is one of the biggest differences between shallow and strong design.
Step 5: Place behavior where the relevant knowledge is
This is one of the deepest OOD rules.
Behavior should usually live where the needed data and rules already exist.
Example:
Bad:
OrderService.cancel(order)- service reaches inside order fields and changes status directly
Better:
order.cancel()- order checks whether cancellation is allowed
Why better:
- the rule stays with the object that owns the state
- callers cannot bypass the logic easily
This does not mean every behavior belongs inside entities. Coordination logic may still belong in services. But core domain rules should not be scattered without reason.
Step 6: Separate domain behavior from orchestration
A common source of confusion is the difference between:
- domain logic
- orchestration logic
Domain logic
Rules intrinsic to the business concept.
Example:
- whether an order can be cancelled
- how an invoice total is calculated
- whether a seat is available
Orchestration logic
The coordination of multiple objects or external systems.
Example:
- create order
- reserve inventory
- charge payment
- send confirmation
This orchestration may belong in an application service such as:
CheckoutServiceReservationServiceRegistrationService
This is a crucial idea.
Good OOD does not mean “no services.”
It means:
- keep business rules near the domain
- use services to coordinate workflows across objects
Step 7: Use interfaces at system edges
External dependencies should often sit behind interfaces.
Examples:
PaymentGatewayEmailSenderInventoryProviderUserRepository
This keeps the core design independent from infrastructure details.
Example:
CheckoutService
-> PaymentGateway
-> OrderRepository
-> NotificationSenderThe core flow depends on contracts, not vendor-specific code.
That is strong design thinking.
Step 8: Design for likely change, not imaginary change
This is where many candidates over-engineer.
Do not build ten abstractions for changes that may never happen.
Instead ask:
- what variability is realistic?
- what change is expensive if unsupported?
Good examples of reasonable flexibility:
- multiple payment providers
- multiple notification channels
- pluggable discount calculation
Bad examples of unnecessary abstraction:
- interface for a class with one stable in-memory implementation and no real reason to vary
OOD should be flexible, but not abstract for its own sake.
Example 1: Designing an order system
Let us design this conceptually.
Requirements:
- customer adds products to cart
- customer checks out
- system creates order
- system charges payment
- order can be cancelled before shipment
Naive design
You might create:
OrderServicePaymentServiceCartService
and then put almost all behavior there.
The result:
- entities become dumb data containers
- rules spread out
- order lifecycle becomes hard to protect
Better design
Domain objects:
CartCartItemOrderOrderItemCustomer
Coordinating service:
CheckoutService
Boundary interfaces:
PaymentGatewayInventoryServiceOrderRepository
Responsibilities:
Cart
- add item
- remove item
- compute subtotal
- produce orderable items
Order
- own ordered items
- know its status
- allow cancellation only in valid states
- compute total if needed
CheckoutService
- validate checkout request
- ask inventory to reserve
- create order
- charge payment
- persist order
- trigger notification
This split is much stronger because:
- the order protects its own lifecycle rules
- the checkout flow coordinates multiple collaborators
- external dependencies are isolated behind interfaces
Key lesson
OOD succeeds when you separate:
- what an object inherently is responsible for
- from what the overall use case must coordinate
Example 2: Designing a library system
Requirements:
- member can borrow a book copy
- a copy can be unavailable
- due dates matter
- returned books close loans
Core concepts
MemberBookBookCopyLoan
Good responsibility assignment
BookCopy
- knows whether it is available
- can be marked borrowed or returned
Loan
- stores loan date and due date
- can determine overdue status
- can close on return
Member
- can request borrowing in a domain-friendly way
Possible coordinating service:
CirculationService
Why not put everything in LibraryService?
Because then:
- copy availability rules get scattered
- loan lifecycle rules become procedural
- the domain becomes weak and harder to evolve
Better:
- the service coordinates
- the domain objects enforce their own rules
Example 3: Designing a parking lot
This is a useful example because it exercises modeling, state, and extensibility.
Requirements:
- vehicles enter and leave
- spots have types
- only compatible vehicles can park in certain spots
- ticket issued on entry
- fee calculated on exit
Core concepts
VehicleParkingSpotTicketParkingLotFeePolicy
Design thinking
ParkingSpot
- knows its type
- knows whether it is occupied
- can accept or reject a given vehicle type
Ticket
- knows entry time
- knows associated vehicle or spot
- can support fee calculation inputs
FeePolicy
- encapsulates pricing rules
ParkingLot
- manages collections of spots
- finds suitable spots
- allocates and releases spots
Possible service:
EntryExitService
Strong OOD move
Instead of hard-coding fee logic into ParkingLot, create a FeePolicy abstraction.
This is better because pricing rules are a separate concern and may change independently.
Example 4: Designing a notification system
Requirements:
- system sends alerts by email, SMS, or push
- different events may trigger notifications
- delivery mechanism may vary
Core concepts
NotificationNotificationTemplateNotificationSender
Concrete implementations:
EmailSenderSmsSenderPushSender
Good OOD insight
This is a place where interface-based design is valuable.
The core system should not depend directly on:
- SMTP details
- SMS vendor APIs
- push SDK details
Instead it depends on a NotificationSender contract.
That is a clean use of polymorphism.
Polymorphism: what it really means in design
Polymorphism means different objects can be used through the same interface while behaving differently.
Example:
CardPaymentGatewayUpiPaymentGatewayPaypalPaymentGateway
All can support:
charge(amount)
Why this matters:
- the caller uses a stable contract
- implementations vary cleanly
- new variants can be added with less disruption
Polymorphism is not just a language feature.
It is a design tool for managing variation.
Inheritance: useful, but dangerous if overused
Inheritance is appropriate when:
- there is a real “is-a” relationship
- shared behavior is conceptually valid
- substitutability holds
Bad inheritance example:
Bird -> Penguin, where code assumes every bird can fly
This is a classic design problem.
If child types break parent assumptions, the hierarchy is wrong.
Prefer inheritance carefully and sparingly.
Many designs are better with:
- composition
- interfaces
- strategies
The SOLID principles in practical terms
You do not need to memorize slogans only. You should understand what each principle protects.
1. Single Responsibility Principle
A class should have one coherent reason to change.
Meaning:
- do not mix unrelated responsibilities
Bad:
InvoiceManagercreates invoices, sends emails, formats PDFs, and syncs tax data
Better:
- separate those concerns
2. Open/Closed Principle
Software should be open for extension but closed for modification where practical.
Meaning:
- adding a new behavior should not require rewriting stable core logic unnecessarily
Example:
- new discount strategy added via
PricingStrategy
3. Liskov Substitution Principle
Subtypes should behave in ways that do not break expectations of the parent type.
Meaning:
- if a
PaymentGatewayis expected, any concrete gateway should work correctly under that contract
4. Interface Segregation Principle
Prefer smaller focused interfaces over one huge interface.
Meaning:
- clients should not depend on methods they do not need
5. Dependency Inversion Principle
High-level policy should depend on abstractions, not low-level details.
Meaning:
- checkout flow depends on
PaymentGateway, not on direct Stripe code
These principles are useful as review tools, not as rigid laws.
Common object-oriented design patterns you should recognize
You should understand the purpose of common patterns conceptually.
Strategy
Encapsulates interchangeable behavior.
Examples:
- pricing
- fee calculation
- routing policy
Factory
Centralizes object creation when construction is complex or variant-based.
Examples:
- creating notification senders
- creating vehicle objects from input type
Adapter
Wraps an external system in a cleaner internal interface.
Examples:
- payment provider wrapper
- third-party email SDK wrapper
Observer or event-based notification
Lets one part of the system react to events from another.
Examples:
- order placed → send email
- booking confirmed → trigger calendar update
Repository
Represents persistence access behind a domain-friendly abstraction.
Examples:
OrderRepositoryUserRepository
You do not need to force patterns into every design. But you should recognize when they solve a real problem.
A practical design approach
When designing an object-oriented system, a useful working sequence is:
- clarify requirements and constraints
- identify main entities and flows
- assign responsibilities
- distinguish domain logic from coordinating services
- discuss relationships and state transitions
- define key interfaces to external dependencies
- call out extensibility points only where realistic
- mention tradeoffs
This sequence helps keep the design grounded in the domain instead of drifting into arbitrary class creation or pattern collection.
Common weak design patterns
Weak designs often:
- jump straight into classes without clarifying the problem
- list entities but not responsibilities
- create a giant
SystemorManagerclass - overuse inheritance
- ignore invariants and state changes
- add abstractions without justification
- never explain tradeoffs
These usually signal shallow modeling and unclear ownership.
What strong designs usually look like
Strong designs usually:
- start with the domain
- identify core entities and their responsibilities
- place rules near the relevant objects
- use services for orchestration
- use interfaces at boundaries
- mention cohesion, coupling, and extensibility
- explain why the design is maintainable
A practical checklist for designing with OOD
When designing from scratch, ask:
- What are the main concepts in the domain?
- What responsibilities belong to each concept?
- What invariants must always hold?
- Which object should own each rule?
- Which workflows require coordination across many objects?
- Where should services exist?
- Where are the boundaries to external systems?
- Which dependencies should be interfaces?
- Where is composition better than inheritance?
- What parts are likely to change?
If you can answer these cleanly, your design will usually be solid.
A warning about overengineering
Many people learn OOD and immediately make designs too abstract.
Symptoms:
- too many interfaces
- too many layers
- tiny classes with no meaningful role
- patterns used for prestige rather than need
Good OOD is not maximal abstraction.
Good OOD is the simplest structure that keeps responsibilities clear and future change manageable.
That balance matters.
From beginner to strong designer
A beginner thinks:
- what classes should I create?
A stronger designer thinks:
- what are the core concepts?
- what rules matter?
- where should behavior live?
- how do parts collaborate without becoming tightly coupled?
- where do I need flexibility and where do I not?
That mindset shift is the real path to becoming good at object-oriented design.
What is enough to say you understand OOD
You understand object-oriented design at a practical level if you can:
- model systems as cooperating objects with clear responsibilities
- keep data and behavior meaningfully connected
- separate domain rules from orchestration
- design stable interfaces around changing details
- prefer composition where it fits better than inheritance
- control coupling and maintain cohesion
- explain tradeoffs clearly
That is enough to design many real systems competently.
Bottom line
Object-oriented design is not mainly about classes, inheritance, or UML notation.
It is about:
- modeling meaningful concepts
- assigning responsibilities well
- protecting invariants
- designing clean collaborations
- managing change through good boundaries
If you learn to:
- identify the right objects
- place behavior where it belongs
- use services for orchestration
- depend on abstractions at system edges
- balance cohesion, coupling, and extensibility
then you move from simply writing object-oriented code to actually designing object-oriented systems well.