Building a Domain-Driven Shopping Cart in C#

A practical guide to modeling a shopping cart with discounts using clean DDD principles in .NET

Posted by Hüseyin Sekmenoğlu on May 21, 2025 Backend Development

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.