Our OOP Journey So Far
Over the preceding lessons, we mastered each fundamental building block in isolation:
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:
Explore available merchandise, descriptions, and current prices.
Collect items with desired quantities in a temporary shopping basket.
Finalize cart selections into a formal purchase order with price locking.
Charge the payment gateway, deduct inventory, and issue confirmation receipts.
02 Steps 1 & 2: Candidate Concepts vs. Core Entities
Reading through the business specifications reveals initial candidate concepts:
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?
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.
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.
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:
An order's core job is managing line items, computing totals, and tracking status. Calling payment APIs or generating PDF invoices violates Single Responsibility!
- 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
06 Steps 7–10: Applying the 4 OOP Pillars in Practice
Every pillar of OOP shines when applied purposefully to address concrete architectural challenges:
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.
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.
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).
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:
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. 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?