Lesson 1 of 2 | Policies, State, and Invariants
Choose a Pricing Strategy Without Hiding Business Rules
Start with the variation
A checkout needs to price the same basket under different policies: standard pricing, a loyalty discount, or a promotion. Strategy isolates the calculation that varies behind a common contract. The checkout supplies a validated subtotal and receives a price; it does not need a growing switch statement for every promotion.
Define the contract before the interface
For this workshop, the subtotal is a nonnegative decimal amount in a currency with two fractional digits. Reject excess precision at the boundary rather than silently changing the subtotal. A discount policy must return an amount between zero and the subtotal. Rounding occurs once at the final price, using an explicitly chosen midpoint rule. Other currencies may need different precision; two digits is a stated example rule, not a universal currency rule.
A minimal C# contract can be expressed as decimal Apply(decimal subtotal). A small pure policy can be a delegate. An interface becomes useful when policy implementations have configuration, dependencies, or several related operations. Neither choice removes the need for input validation.
Work through a concrete basket
For a validated subtotal of 250.00 and a 10% loyalty discount, the unrounded price is 250.00 × 0.90 = 225.00. A fixed discount of 40.00 yields 210.00. A fixed discount of 300.00 is clamped to zero if that is the agreed business rule; alternatively reject the promotion. Document the choice.
The policy selector chooses exactly one applicable policy unless stacking is explicitly supported. Strategy does not decide whether coupons can combine, which customer is eligible, or whether a promotion has expired. Those are separate business rules. Hiding them in a constructor makes the design harder to test and explain.
| Decision | Good default | Why |
|---|---|---|
| Small stateless calculation | Delegate or pure function | Little ceremony; easy to test |
| Configured calculation family | Strategy interface | Named implementations and dependencies |
| Two fixed branches unlikely to grow | A clear conditional | Avoid premature abstraction |
| Sequential order stages | State transitions | Behavior depends on lifecycle, not a freely chosen policy |
Tests that protect the contract
Check zero subtotal, a normal subtotal, a zero discount, a full discount, invalid negative input, and an invalid discount above 100%. Add a fractional input such as 0.005 to verify the agreed precision rule. Assert that results never become negative or exceed the validated subtotal. Run the same contract tests against every interchangeable policy.
A decimal type avoids many binary floating-point representation surprises for base-ten amounts, but it does not define rounding policy, currency, tax, or precision for you. Keep those decisions explicit.
Exercise
Design a promotion that discounts 15% only when the subtotal is at least 500.00. Explain whether eligibility belongs in the selector or the policy. Then specify outputs for 499.99, 500.00, and 800.00.
Worked solution: One reasonable design lets the policy own the threshold so it can be tested as a complete promotion. It returns 499.99, 425.00, and 680.00 respectively. The selector still verifies that this promotion is permitted for the customer and cannot conflict with another chosen promotion.
Interview follow-ups
- How would you version policies so an old order can explain its original price? Store the selected policy identifier, relevant configuration, and computed totals with the order.
- Should a strategy fetch customer data during calculation? Prefer passing validated facts into a pure calculation; keep I/O boundaries visible.
- When is Strategy overkill? When variation is small, stable, and clearer as a direct conditional.
The success criterion is a visible business contract with replaceable calculations, not the number of interfaces.