Creating a shopping cart that reflects real-world business logic requires more than just basic CRUD operations. In this article, we will walk through the design and implementation of a shopping cart in C# using Domain-Driven Design (DDD). You will see how to track item totals, apply different types of discounts and maintain clean separation of concerns.
TLDR: Here is the github repo: https://github.com/sekmenhuseyin/ShoppingCardDomain
🧠 Why Domain-Driven Design?
Domain-Driven Design (DDD) helps manage complexity by modeling the real-world domain in software. In this article, we’ll build a simple Shopping Cart system in C# using DDD principles. You’ll learn how to design clean, extensible components that naturally reflect business rules.
📐 Project Structure
We split our code into meaningful layers:
ShoppingCart/
├── Entities/
│   ├── Cart.cs
│   ├── CartItem.cs
│   └── Product.cs
├── ValueObjects/
│   └── Money.cs
├── Discounts/
│   ├── IDiscount.cs
│   ├── PercentageDiscount.cs
│   ├── FixedAmountDiscount.cs
│   └── QuantityBasedDiscount.cs
🧱 Core Concepts
💰 Money (Value Object)
A value object that encapsulates currency-safe price operations.
public record Money(decimal Amount, string Currency)
{
    public static Money Zero(string currency) => new(0, currency);
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return new Money(Amount + other.Amount, Currency);
    }
    public Money Multiply(int quantity) => new(Amount * quantity, Currency);
}
📦 Product (Entity)
A purchasable item that may have a discount policy.
public class Product
{
    public Guid Id { get; }
    public string Name { get; }
    public Money Price { get; }
    public IDiscount? Discount { get; }
    public Product(Guid id, string name, Money price, IDiscount? discount = null)
    {
        Id = id;
        Name = name;
        Price = price;
        Discount = discount;
    }
}
🎯 Applying Discounts
Each product may define its own discount logic using the IDiscount interface.
📉 Percentage Discount
public class PercentageDiscount : IDiscount
{
    private readonly decimal _percentage;
    public PercentageDiscount(decimal percentage) => _percentage = percentage;
    public Money Apply(Product product, int quantity)
    {
        var total = product.Price.Multiply(quantity);
        var discount = total.Amount * (_percentage / 100);
        return new Money(total.Amount - discount, product.Price.Currency);
    }
}
💵 Fixed Amount Discount
public class FixedAmountDiscount : IDiscount
{
    private readonly decimal _amount;
    public FixedAmountDiscount(decimal amount) => _amount = amount;
    public Money Apply(Product product, int quantity)
    {
        var total = product.Price.Multiply(quantity);
        var discounted = total.Amount - _amount;
        return new Money(Math.Max(0, discounted), product.Price.Currency);
    }
}
🎁 Quantity-Based Discount (3 for 2)
public class QuantityBasedDiscount : IDiscount
{
    private readonly int _required;
    private readonly int _payFor;
    public QuantityBasedDiscount(int required, int payFor)
    {
        _required = required;
        _payFor = payFor;
    }
    public Money Apply(Product product, int quantity)
    {
        int sets = quantity / _required;
        int remainder = quantity % _required;
        int totalUnits = sets * _payFor + remainder;
        return product.Price.Multiply(totalUnits);
    }
}
🛒 Cart and Cart Items
🧩 CartItem
public class CartItem
{
    public Product Product { get; }
    public int Quantity { get; private set; }
    public CartItem(Product product, int quantity)
    {
        Product = product;
        Quantity = quantity;
    }
    public void IncreaseQuantity(int amount) => Quantity += amount;
    public Money GetTotal()
    {
        if (Product.Discount != null)
            return Product.Discount.Apply(Product, Quantity);
        return Product.Price.Multiply(Quantity);
    }
}
🧠 Cart (Aggregate Root)
public class Cart
{
    private readonly List<CartItem> _items = new();
    public IReadOnlyCollection<CartItem> Items => _items;
    public void AddItem(Product product, int quantity = 1)
    {
        var existing = _items.FirstOrDefault(i => i.Product.Id == product.Id);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(new CartItem(product, quantity));
    }
    public int TotalItemCount => _items.Sum(i => i.Quantity);
    public Money GetTotalPrice()
    {
        if (!_items.Any()) return Money.Zero("USD");
        var total = Money.Zero(_items[0].Product.Price.Currency);
        foreach (var item in _items)
            total = total.Add(item.GetTotal());
        return total;
    }
}
🧪 Example Usage
var apple = new Product(Guid.NewGuid(), "Apple", new Money(1.0m, "USD"), new QuantityBasedDiscount(3, 2));
var cart = new Cart();
cart.AddItem(apple, 3);
Console.WriteLine($"Total Items: {cart.TotalItemCount}");       // 3
Console.WriteLine($"Total Price: {cart.GetTotalPrice().Amount}"); // 2
✅ Conclusion
By following the DDD approach, we created a robust and extensible shopping cart system. Each concept like discounts, items and prices is cleanly encapsulated and easy to evolve.
You can now extend this project with features like:
- Removing items 
- Supporting multiple currencies 
- Saving the cart to a database 
This structure allows change without chaos, which is the core idea of Domain-Driven Design.