Skip to main content

Module 1: OOD Foundations & Code Smells: Case Studies

Use these cases to practice recognizing design pressure before the code becomes hard to change. Each case asks for a concrete artifact that can be reviewed with another engineer.


Case Study 1: The Checkout Service That Knows Every Rule

Scenario: An ecommerce CheckoutService validates coupons, calculates tax, applies loyalty points, checks inventory, sends email, and writes the order. Every new promotion changes the same file, and every bug fix risks breaking a different checkout path.

Source anchor: Code Smell - Martin Fowler, plus the module's SRP and large-class concepts.

Module concepts:

  • Single Responsibility Principle
  • large class
  • divergent change
  • cohesion and reasons to change

Wrong Approach

Split the file by method name only: CheckoutValidation, CheckoutCalculation, CheckoutPersistence. The class count rises, but the same policy decisions remain tangled across the workflow.

Better Approach

Identify independent reasons to change: pricing policy, inventory reservation, payment authorization, order persistence, and notification. Move policy behavior near the data it owns, leave orchestration in a thin application service, and add behavior tests for representative checkout paths before moving code.

Tradeoff Table

ChoiceGainCost
Keep one serviceSimple call pathHigh regression risk and merge conflicts
Split by technical layerFamiliar namingBusiness rules still scatter
Split by responsibilityClear ownershipRequires tests and careful interface design

Failure Mode

A loyalty-points change modifies tax calculation because both rules share temporary variables in the same long method.

Required Artifact

Create a responsibility map that lists each checkout rule, its reason to change, its owning object or service, and the tests that protect the move.

Project / Capstone Connection

Use the same responsibility map for the Semester 3 refactoring project before applying patterns.


Case Study 2: The Anemic Order Model

Scenario: Order, OrderLine, and Customer classes contain fields and getters only. All rules live in procedural services: OrderService.cancel(order), PricingService.total(order), ReturnService.canReturn(order). Engineers keep adding flags because there is no obvious place for behavior.

Source anchor: Anemic Domain Model - Martin Fowler.

Module concepts:

  • encapsulation
  • tell, do not ask
  • domain behavior
  • feature envy

Wrong Approach

Keep the entities as data records and create another OrderRules class to hold every new conditional.

Better Approach

Move behavior that depends on Order invariants into the model: cancellation rules, total calculation that belongs to order lines, and return eligibility that depends on order state. Keep infrastructure concerns outside the entity, but let the domain object protect its own rules.

Tradeoff Table

ChoiceGainCost
Procedural servicesEasy to startRules duplicate and drift
Behavior-rich modelStrong invariantsNeeds clear boundaries around I/O
Hybrid modelIncremental adoptionCan become inconsistent if ownership is vague

Failure Mode

One service allows cancellation after shipment while another denies it because both reimplemented the rule.

Required Artifact

Write a before/after object responsibility diagram and three behavior tests that prove the model enforces its invariants.

Project / Capstone Connection

Use this case when deciding whether a class in the capstone should be a real domain object or a transport record.


Case Study 3: Primitive Obsession in Billing

Scenario: A subscription system passes amount: number, currency: string, email: string, and country: string through dozens of methods. A refund accidentally mixes cents and dollars, and support finds invoices sent to invalid addresses.

Source anchor: Refactoring catalog - Martin Fowler, especially value-oriented refactorings such as introducing parameter objects and replacing primitives with richer domain concepts.

Module concepts:

  • primitive obsession
  • value objects
  • invalid states
  • interface clarity

Wrong Approach

Add more comments and parameter names: amountInCents, currencyCode, recipientEmail. The compiler still accepts invalid combinations.

Better Approach

Introduce small immutable value objects such as Money, EmailAddress, and CountryCode. Put validation at construction boundaries, make illegal combinations unrepresentable where practical, and keep serialization adapters at the system edge.

Tradeoff Table

ChoiceGainCost
Raw primitivesFast to pass aroundEasy to mix invalid values
Value objectsSafer domain APIMore types and conversion code
Validation only at UIFewer backend changesBackground jobs and APIs can bypass it

Failure Mode

A background refund job bypasses UI validation and sends 19.99 to an API that expects cents.

Required Artifact

Design a Money value object API with construction rules, equality behavior, serialization boundaries, and tests for invalid input.

Project / Capstone Connection

Apply the same value-object audit to identifiers, dates, money, and status fields in the project codebase.


Case Study 4: The Train-Wreck API Call

Scenario: A reporting controller computes customer risk with order.customer.account.profile.preferences.region.policy.riskTier. A schema change in the customer profile breaks reporting, recommendation, and fraud code at once.

Source anchor: Code Smell - Martin Fowler, connected to message chains and excessive coupling.

Module concepts:

  • coupling
  • Law of Demeter
  • message chains
  • information hiding

Wrong Approach

Wrap the chain in a helper called getRiskTier(order) but keep the helper in the controller layer.

Better Approach

Ask which object owns the policy decision. Expose a stable query such as order.customerRiskTier(policyDate) or a domain service that accepts the minimal facts it needs. Hide profile traversal behind a boundary that can change without forcing callers to know the shape.

Tradeoff Table

ChoiceGainCost
Direct traversalQuick accessCallers depend on internal structure
Helper functionReduces duplicationCan hide the same coupling
Owner-level methodStable contractRequires naming the real responsibility

Failure Mode

A profile migration breaks unrelated features because callers reached through internal object graphs.

Required Artifact

Create a dependency sketch showing the current message chain, the proposed owner, and the public method each caller should use.

Project / Capstone Connection

Use this sketch during code review when a feature reaches through three or more collaborators.


Case Study 5: Inheritance That Violates Substitution

Scenario: Square extends Rectangle in a drawing library because both have width and height. A layout algorithm sets width and height independently; squares override setters and surprise callers.

Source anchor: The module's Liskov Substitution lesson and the classic subtype-contract problem.

Module concepts:

  • Liskov Substitution Principle
  • subtype contracts
  • composition over inheritance
  • behavioral compatibility

Wrong Approach

Patch the layout algorithm with if shape is Square checks.

Better Approach

Model shared behavior through an interface such as ResizableShape only when the contract is valid for every implementer. Use composition or separate types when invariants differ. Tests should assert substitutability, not just constructor behavior.

Tradeoff Table

ChoiceGainCost
Inherit for shared fieldsLess code initiallyBroken behavioral contract
Type checksQuick patchSpreads special cases
Smaller interfacesHonest contractsMore explicit design work

Failure Mode

Any caller that assumes independent width and height changes fails when passed a square.

Required Artifact

Write a subtype contract table for Rectangle, Square, and the replacement interface, including preconditions, postconditions, and tests.

Project / Capstone Connection

Use the same contract table before introducing inheritance in the semester project.


Source Map

SourceUse it for
Code Smell - Martin FowlerFraming smells as signals of deeper design problems.
Anemic Domain Model - Martin FowlerDistinguishing data containers from behavior-rich domain objects.
Refactoring catalog - Martin FowlerNaming refactorings that move from primitive-heavy code toward clearer design.