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
| Choice | Gain | Cost |
|---|---|---|
| Keep one service | Simple call path | High regression risk and merge conflicts |
| Split by technical layer | Familiar naming | Business rules still scatter |
| Split by responsibility | Clear ownership | Requires 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
| Choice | Gain | Cost |
|---|---|---|
| Procedural services | Easy to start | Rules duplicate and drift |
| Behavior-rich model | Strong invariants | Needs clear boundaries around I/O |
| Hybrid model | Incremental adoption | Can 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
| Choice | Gain | Cost |
|---|---|---|
| Raw primitives | Fast to pass around | Easy to mix invalid values |
| Value objects | Safer domain API | More types and conversion code |
| Validation only at UI | Fewer backend changes | Background 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
| Choice | Gain | Cost |
|---|---|---|
| Direct traversal | Quick access | Callers depend on internal structure |
| Helper function | Reduces duplication | Can hide the same coupling |
| Owner-level method | Stable contract | Requires 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
| Choice | Gain | Cost |
|---|---|---|
| Inherit for shared fields | Less code initially | Broken behavioral contract |
| Type checks | Quick patch | Spreads special cases |
| Smaller interfaces | Honest contracts | More 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
| Source | Use it for |
|---|---|
| Code Smell - Martin Fowler | Framing smells as signals of deeper design problems. |
| Anemic Domain Model - Martin Fowler | Distinguishing data containers from behavior-rich domain objects. |
| Refactoring catalog - Martin Fowler | Naming refactorings that move from primitive-heavy code toward clearer design. |