A .NET implementation of the classic supermarket checkout kata, demonstrating clean architecture, SOLID principles, and extensible pricing strategies.
Implement a supermarket checkout that:
- Scans items individually (identified by SKU)
- Calculates running totals
- Applies special offers (e.g., "3 for 130", "buy 2 get 1 free")
- Accepts items in any order
- Supports configurable pricing rules
graph TB
subgraph "Application Layer"
ICheckout[ICheckout]
IPricingService[IPricingService]
CheckOutService[CheckOutService]
PricingService[PricingService]
end
subgraph "Domain Layer"
Sku[Sku]
ProductPricing[ProductPricing]
SpecialOffer[SpecialOffer]
subgraph "Special Offers"
MultiPriceOffer[MultiPriceOffer<br/>3 for 130]
BuyXGetYFree[BuyXGetYFreeOffer<br/>Buy 2 Get 1 Free]
ThresholdPercent[ThresholdQuantityPercentOffer<br/>10% off when buying 5+]
TieredPrice[TieredUnitPriceOffer<br/>Volume discounts]
end
end
CheckOutService --> ICheckout
CheckOutService --> PricingService
PricingService --> IPricingService
PricingService --> ProductPricing
ProductPricing --> Sku
ProductPricing --> SpecialOffer
MultiPriceOffer --> SpecialOffer
BuyXGetYFree --> SpecialOffer
ThresholdPercent --> SpecialOffer
TieredPrice --> SpecialOffer
CheckoutKata/
├── CheckoutKata.Domain/ # Pure domain models (no dependencies)
│ ├── Sku.cs # Value object for product identifier
│ ├── ProductPricing.cs # Product with price and optional offer
│ └── SpecialOffers/
│ ├── SpecialOffer.cs # Abstract base (Strategy pattern)
│ ├── MultiPriceOffer.cs # "3 for 130"
│ ├── BuyXGetYFreeOffer.cs # "Buy 2 get 1 free"
│ ├── ThresholdQuantityPercentOffer.cs # "10% off 5+"
│ └── TieredUnitPriceOffer.cs # Volume pricing
│
├── CheckoutKata.Application/ # Use cases and services
│ ├── Interfaces/
│ │ ├── ICheckout.cs # Checkout contract
│ │ └── IPricingService.cs # Pricing calculation contract
│ └── Services/
│ ├── CheckOutService.cs # Stateful checkout implementation
│ └── PricingService.cs # Pricing calculation engine
│
└── CheckoutKata.UnitTests/ # Comprehensive test suite
├── CheckoutTestBase.cs # Shared test infrastructure
├── Catalogs/ # Test pricing configurations
└── Tests/
├── CheckoutTests.cs # Core kata tests
├── SpecialOffersCheckoutTests.cs # Extended offer tests
└── InvalidInputTests.cs # Validation tests
| SKU | Unit Price | Special Offer |
|---|---|---|
| A | 50 | 3 for 130 |
| B | 30 | 2 for 45 |
| C | 20 | - |
| D | 15 | - |
- .NET 9 SDK (or .NET 8+)
dotnet testdotnet test --verbosity normaldotnet build// Create pricing catalog
var products = new[]
{
new ProductPricing(new Sku("A"), 50, new MultiPriceOffer(3, 130)),
new ProductPricing(new Sku("B"), 30, new MultiPriceOffer(2, 45)),
new ProductPricing(new Sku("C"), 20),
new ProductPricing(new Sku("D"), 15)
};
// Create checkout
var checkout = new CheckOutService(products);
// Scan items (any order)
checkout.Scan("A");
checkout.Scan("B");
checkout.Scan("A");
checkout.Scan("A"); // 3 A's now qualify for special
// Get total (applies offers automatically)
var total = checkout.GetTotalPrice(); // 130 + 30 = 160Adding a new offer type is simple:
- Create a class extending
SpecialOffer - Implement
CalculateLineTotal - Use it in your product catalog
public class MyCustomOffer : SpecialOffer
{
public override int CalculateLineTotal(
ProductPricing product,
int quantity,
IReadOnlyDictionary<Sku, int> basketQuantities)
{
// Your pricing logic here
}
}- Strategy Pattern: Special offers are interchangeable pricing strategies
- Value Objects:
Skuensures consistent identity (trimmed, uppercase) - Separation of Concerns: Domain logic is isolated from application services
- Fail Fast: Guard clauses validate inputs immediately
- Immutable Configuration: Pricing rules are set at construction
- 32 tests covering:
- Single items and combinations
- All special offer types
- Order-independent scanning
- Incremental totals
- Input validation and edge cases