Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Strategy and State Patterns: A Practical C# Workshop: back to the course

    Module 1 · Policies, State, and Invariants · Lesson 1 of 2

    Choose a Pricing Strategy Without Hiding Business Rules

    Watch

    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.

    DecisionGood defaultWhy
    Small stateless calculationDelegate or pure functionLittle ceremony; easy to test
    Configured calculation familyStrategy interfaceNamed implementations and dependencies
    Two fixed branches unlikely to growA clear conditionalAvoid premature abstraction
    Sequential order stagesState transitionsBehavior 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.

    Worked rounding decision

    For a valid subtotal of 10.10 with 15% off, the unrounded payable total is 8.585. At two decimal places, ToEven gives 8.58 and AwayFromZero gives 8.59. Choose the rule in the business contract and check the returned total, not only the unrounded intermediate. These are conceptual arithmetic examples, not a runnable C# program.

    Sources and optional video

    Milan Jovanović: Replace Switch Statements with the Strategy Pattern in C# (2026) is a C# shipping-calculation example that separates interchangeable algorithms from dictionary-based selection. Apply that separation to this lesson’s pricing policies. The lesson’s eligibility, input precision, output bounds and rounding rules remain explicit application contracts. Reviewed against the creator’s published description; SDK version and video playback were not verified.

    Pricing analogy

    Think of a checkout using interchangeable discount recipe cards. Every card accepts the same validated subtotal and returns a payable total under the same rules. A separate cashier rulebook decides which card is eligible and takes precedence; the card performs the arithmetic. This analogy covers choosing a pricing calculation, not changing an order's lifecycle. It also does not imply that multiple cards can be combined: stacking needs its own explicit rule.

    Pricing cheat sheet

    • Variation: standard, loyalty and promotion arithmetic can share a subtotal-to-payable-total contract. Keep eligibility, expiry, precedence, fallback and any stacking decisions explicit in the selecting workflow.
    • Inputs: use decimal for these base-10 amounts; reject negative subtotals and unsupported precision. This example accepts at most two fractional digits. Decimal itself permits other scales and has finite precision and range.
    • Outputs: the final payable total must be at least zero and no greater than the accepted subtotal. Standard pricing returns that subtotal unchanged.
    • Rounding: round only the final payable total and name the midpoint rule. At 10.10 with 15% off, 8.585 becomes 8.58 with ToEven or 8.59 with AwayFromZero.
    • Configuration: validate rate units and bounds. An excessive fixed discount requires an explicit zero-floor or rejection rule; do not silently invent one.
    • Boundaries: 15% off at subtotal ≥ 500.00 yields 499.99 → 499.99, 500.00 → 425.00 and 800.00 → 680.00.
    • Design: keep I/O visible. A pure delegate suits small calculations; an interface can suit policies with configuration or dependencies. Two stable branches may remain a conditional.
    • Verification: reuse shared contract tests, then add policy examples. Preserve policy ID/version/configuration, accepted inputs, rounding rule and computed totals for later explanation.

    Practice

    Sign in to mark lessons done and keep your place in the course.Sign in