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.