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:

  1. model a system as a set of cooperating objects
  2. give each object a clear responsibility
  3. keep data and behavior related
  4. let objects collaborate through well-defined interfaces
  5. 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 Order objects may exist
  • they are all created from the Order class 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 Order may 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:

  • PaymentGateway
  • NotificationSender
  • PricingStrategy

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:

  • Cart is responsible for holding selected items
  • Order is responsible for enforcing order rules
  • PaymentProcessor is responsible for coordinating payment flow
  • InventoryService is 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 details
  • Repository.save() hides storage details
  • NotificationSender.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.status freely
  • 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:

  • UserManager that 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 PremiumDiscountOrder and FestivalDiscountOrder
  • use a PricingStrategy object

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:

  • CheckoutService depends on PaymentGateway
  • 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:

  1. understand the domain
  2. identify the important concepts
  3. identify the key behaviors and rules
  4. assign responsibilities
  5. design collaborations
  6. protect invariants
  7. define boundaries to external systems
  8. 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:

  • Customer
  • Cart
  • Order
  • OrderItem
  • Product
  • Payment
  • Inventory

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:

  • CheckoutService
  • ReservationService
  • RegistrationService

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:

  • PaymentGateway
  • EmailSender
  • InventoryProvider
  • UserRepository

This keeps the core design independent from infrastructure details.

Example:

CheckoutService
  -> PaymentGateway
  -> OrderRepository
  -> NotificationSender

The 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:

  • OrderService
  • PaymentService
  • CartService

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:

  • Cart
  • CartItem
  • Order
  • OrderItem
  • Customer

Coordinating service:

  • CheckoutService

Boundary interfaces:

  • PaymentGateway
  • InventoryService
  • OrderRepository

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

  • Member
  • Book
  • BookCopy
  • Loan

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

  • Vehicle
  • ParkingSpot
  • Ticket
  • ParkingLot
  • FeePolicy

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

  • Notification
  • NotificationTemplate
  • NotificationSender

Concrete implementations:

  • EmailSender
  • SmsSender
  • PushSender

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:

  • CardPaymentGateway
  • UpiPaymentGateway
  • PaypalPaymentGateway

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:

  • InvoiceManager creates 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 PaymentGateway is 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:

  • OrderRepository
  • UserRepository

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:

  1. clarify requirements and constraints
  2. identify main entities and flows
  3. assign responsibilities
  4. distinguish domain logic from coordinating services
  5. discuss relationships and state transitions
  6. define key interfaces to external dependencies
  7. call out extensibility points only where realistic
  8. 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 System or Manager class
  • 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:

  1. What are the main concepts in the domain?
  2. What responsibilities belong to each concept?
  3. What invariants must always hold?
  4. Which object should own each rule?
  5. Which workflows require coordination across many objects?
  6. Where should services exist?
  7. Where are the boundaries to external systems?
  8. Which dependencies should be interfaces?
  9. Where is composition better than inheritance?
  10. 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.