20 min read System Modeling Lesson 10 of 12
Course 04 · Part 3 — Object Relationships & Architecture

Modeling Real-World Systems with OOP

Translating ambiguous problem statements into classes, responsibilities, and collaborative pipelines

In professional software engineering, clients and product leaders will never tell you—“create a class here, or apply polymorphism there.” Instead, they present you with an ambiguous business requirement: “We need an e-commerce platform where customers buy items and receive confirmations.” Deciding which objects should exist, who holds what state, and how they interact to fulfill the workflow is the true craft of Object-Oriented Modeling.

Our OOP Journey So Far

Over the preceding lessons, we mastered each fundamental building block in isolation:

Class → Object → State & Behavior → Encapsulation → Abstraction → Inheritance → Polymorphism → Composition → Collaboration & Modeling

Today, we will synthesize all of these concepts to design a resilient, end-to-end domain model for a production-grade problem.

01 What Are We Building? (The Problem Statement)

Suppose we are tasked with engineering the core domain engine of an e-commerce shopping platform. From the customer's perspective:

1
Browse Products:

Explore available merchandise, descriptions, and current prices.

2
Add to Cart:

Collect items with desired quantities in a temporary shopping basket.

3
Create an Order:

Finalize cart selections into a formal purchase order with price locking.

4
Payment & Fulfillment:

Charge the payment gateway, deduct inventory, and issue confirmation receipts.

Caution: Opening an IDE and coding immediately upon reading requirements is the single most common junior developer pitfall. System design starts with paper, conceptual discovery, and invariant analysis.

02 Steps 1 & 2: Candidate Concepts vs. Core Entities

Reading through the business specifications reveals initial candidate concepts:

Customer Product Cart CartItem Order OrderItem Payment

These are not final classes yet—they are Candidate Concepts. Next, we determine the essential state (attributes) and domain responsibilities for each candidate entity:

Entity Core State (Attributes) Domain Purpose
Customer id, name, email The purchaser engaging with the store
Product id, name, price, stock Sellable merchandise and catalog pricing
Cart items (list of CartItem) Temporary working basket prior to checkout
Order order_id, items, total, status Immutable transaction contract between buyer & store

03 Step 3: Why CartItem and OrderItem Must Be Separate Classes

At first glance, both objects look identical: a reference to an item plus a quantity. Why not merge them into a single class?

CartItem (Ephemeral & Dynamic)

A customer adds a laptop to their cart today and checks out 3 days later. A CartItem dynamically references the live Product object. If the product price increases tomorrow, reloading the cart automatically reflects the new price. Carts are easily discarded and highly volatile.

CartItem: product_ref + quantity
OrderItem (Immutable Historical Snapshot)

Once submitted, an order is a legally binding financial record. If you purchase headphones for $50 today and the manufacturer raises the catalog price to $75 next week, your customer receipt must still say $50! An OrderItem must take an unalterable price snapshot at the exact second of purchase.

OrderItem: product_id + quantity + unit_price_at_purchase
Golden Rule of Domain Modeling: Just because two objects share identical attributes in memory does NOT mean they are the same concept. If their lifecycles, business meanings, and immutability contracts differ, they belong in separate classes!

04 Steps 4 & 5: Responsibility Assignment & Avoiding the God Object

A common trap when modeling is overloading the Order class with every business operation in the system, creating a monstrous God Object:

Bad Design: The Bloated God Order
class Order:
- processPayment()
- sendConfirmationEmail()
- updateDatabaseInventory()
- calculateCourierShipping()
- generatePDFInvoice()

An order's core job is managing line items, computing totals, and tracking status. Calling payment APIs or generating PDF invoices violates Single Responsibility!

Good Design: Specialized Collaborating Services
  • Order: Holds order state, items list, and computes calculate_total().
  • PaymentService: Charges credit cards or mobile wallets via gateways.
  • InventoryService: Checks stock reservations and decrements warehouse inventory.
  • NotificationService: Dispatches confirmation emails and SMS receipts.

05 Step 6: Tracking Use Cases & Orchestration

When the customer taps "Confirm & Pay", who ensures these independent components fire in the exact right sequence? Enter Orchestration.

The CheckoutService Orchestration Pipeline

1 Cart Validation: Verify that the cart is not empty and requested stock is reserved.
2 Order Creation: Convert ephemeral CartItems into frozen, price-snapshotted OrderItems.
3 Payment Execution: Delegate the charging workflow to the injected PaymentService.
4 State & Stock Transition: Mark Order as 'PAID' and notify InventoryService to commit stock decrements.
5 Cleanup & Alert: Clear the shopping cart and instruct NotificationService to email the invoice.

06 Steps 7–10: Applying the 4 OOP Pillars in Practice

Every pillar of OOP shines when applied purposefully to address concrete architectural challenges:

Encapsulation Guarding Order State Invariants

External code cannot arbitrarily execute order.status = "DELIVERED". State changes occur strictly via controlled domain methods (order.mark_as_paid(), order.ship()), guaranteeing that an order cannot jump straight to Delivered without payment verification.

Abstraction & Polymorphism Pluggable Payment Gateways

PaymentMethod defines an abstract interface requiring a pay(amount) contract. Whether a user chooses CardPayment, BkashPayment, or BankTransfer, the CheckoutService invokes the identical interface without knowing the internal implementation details.

Composition Building Whole Systems from Discrete Parts

An Order strongly owns its OrderItems (Composition—deleting the order eliminates the item list). Meanwhile, CheckoutService receives its PaymentService and InventoryService via constructor injection (Loose Coupling).

Inheritance (Where Appropriate) Physical vs. Digital Products

Only when genuine behavioral variance exists—such as PhysicalProduct requiring weight calculations and shipping fees, whereas DigitalProduct has zero shipping fee and generates download links—does inheritance with polymorphic method overrides become justified.

07 Unified System Architecture Graph

Here is the synthesized object graph illustrating the core relationships and collaboration pathways:

                             Customer
                                │
                     ┌──────────┴──────────┐
                     │                     │
                     ▼                     ▼
                    Cart                 Order (State Machine)
                     │                     │
                     ▼ (contains)          ▼ (contains)
                 CartItem[]           OrderItem[] (Price Snapshot)
                     │                     │
                     ▼ (references)        ▼ (references)
                  Product               Product
                                           ▲
                                           │ (specialized)
                                    ┌──────┴──────┐
                                    │             │
                              PhysicalProduct  DigitalProduct

              CheckoutService (Orchestrator)
                     │
         ┌───────────┼───────────┬───────────┐
         ▼           ▼           ▼           ▼
        Cart       Order   PaymentService InventoryService
                                 │
                                 ▼ (delegates)
                           PaymentMethod <<interface>>
                             ┌───┴───┐
                             ▼       ▼
                          CardPay  BkashPay
        

08 Practical Python Implementation

Notice how effortless and legible implementation becomes once the conceptual model is mapped clearly on paper:

ecommerce_model.py Domain Entities & Checkout Orchestration
from abc import ABC, abstractmethod

# 1. Product and CartItem
class Product:
    def __init__(self, product_id: str, name: str, price: float):
        self.id = product_id
        self.name = name
        self.price = price

class CartItem:
    def __init__(self, product: Product, quantity: int):
        self.product = product
        self.quantity = quantity

    def subtotal(self) -> float:
        return self.product.price * self.quantity

class Cart:
    def __init__(self):
        self.items: list[CartItem] = []

    def add_item(self, product: Product, quantity: int = 1):
        for item in self.items:
            if item.product.id == product.id:
                item.quantity += quantity
                return
        self.items.append(CartItem(product, quantity))

    def calculate_total(self) -> float:
        return sum(item.subtotal() for item in self.items)

    def clear(self):
        self.items.clear()

# 2. OrderItem (with Immutable Price Snapshot) & Order
class OrderItem:
    def __init__(self, product_id: str, product_name: str, quantity: int, unit_price: float):
        self.product_id = product_id
        self.product_name = product_name
        self.quantity = quantity
        self.unit_price = unit_price  # Price locked permanently at moment of purchase

    def subtotal(self) -> float:
        return self.unit_price * self.quantity

class Order:
    def __init__(self, order_id: str, items: list[OrderItem]):
        self.order_id = order_id
        self.items = items
        self.status = "PENDING"

    def calculate_total(self) -> float:
        return sum(item.subtotal() for item in self.items)

    def mark_as_paid(self):
        if self.status != "PENDING":
            raise ValueError(f"Invalid state transition: Cannot mark {self.status} order as PAID")
        self.status = "PAID"

# 3. Payment Abstraction & Implementations
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        pass

class BkashPayment(PaymentMethod):
    def __init__(self, phone: str):
        self.phone = phone

    def pay(self, amount: float) -> bool:
        print(f"[bKash] Charged ${amount:.2f} successfully from wallet {self.phone}.")
        return True

# 4. Checkout Orchestration Service
class CheckoutService:
    def __init__(self, payment_method: PaymentMethod):
        self.payment_method = payment_method

    def checkout(self, cart: Cart, order_id: str) -> Order:
        if not cart.items:
            raise ValueError("Cannot checkout an empty shopping cart!")

        # Transform volatile CartItems into immutable, price-snapshotted OrderItems
        order_items = [
            OrderItem(item.product.id, item.product.name, item.quantity, item.product.price)
            for item in cart.items
        ]
        order = Order(order_id, order_items)
        total_amount = order.calculate_total()

        # Delegate charging to the payment gateway
        payment_success = self.payment_method.pay(total_amount)
        if payment_success:
            order.mark_as_paid()
            cart.clear()
            print(f"Order {order.order_id} processed successfully! Status: {order.status}")
            return order
        else:
            raise RuntimeError("Payment transaction declined. Please try another method.")

# 5. Executing the Simulation
keyboard = Product("P101", "Mechanical Keyboard", 85.00)
mouse = Product("P102", "Wireless Mouse", 35.00)

my_cart = Cart()
my_cart.add_item(keyboard, 1)
my_cart.add_item(mouse, 2)

bkash = BkashPayment("+15550198")
checkout_engine = CheckoutService(bkash)

# Run checkout
confirmed_order = checkout_engine.checkout(my_cart, "ORD-9901")
print(f"Items remaining in cart: {len(my_cart.items)}")

09 10-Point Practical Domain Modeling Checklist

Whenever you receive a fresh feature specification or architectural problem, walk through these 10 guiding questions before coding:

1. What are the key real-world concepts (Nouns) in the business problem?
2. Which concepts own distinct, independent state attributes?
3. Which concepts exhibit meaningful behaviors and actions (Verbs)?
4. What are individual responsibilities? Is any single class turning into a God Object?
5. What relationships exist between objects (Association, Composition, Dependency)?
6. Do two similarly structured objects differ in lifecycles or immutability rules?
7. Where are multiple variants expected, requiring interface abstraction?
8. Where should you favor composition over deep inheritance hierarchies?
9. Are state invariants and business logic shielded behind encapsulation methods?
10. Is an Orchestrator service needed to coordinate the multi-step business workflow?
Concept Check: Domain Modeling Quiz

1. Why should CartItem and OrderItem be modeled as two separate classes instead of reusing one?

2. What is the primary role of an orchestrator class like CheckoutService?

Key Takeaways

Noun ≠ Class Fallacy: Not every noun in requirements becomes a class; only concepts with meaningful state, lifecycle, and domain purpose qualify.
Snapshotting Invariants: Historical records (like OrderItem) and active staging items (like CartItem) must have distinct models to ensure data integrity.
Orchestration over Bloat: Prevent God Objects by delegating operations to specialized domain services and coordinating them via orchestrators.
OOP as a Thinking Framework: OOP is not about syntax dogmatism—it is an architectural strategy for decomposing chaos into clear, collaborating components.