Structural design patterns deal with how objects are created in terms of their structure and hierarchy.
One of the patterns that explicitly helps you manage complex hierarchical scenarios is the Composite Design Pattern.
So what does this pattern really do?
Consider a dataset where you want a common interface responsible for managing it, ensuring one method is used for everything. You also want to allow a blueprint to manage this data and allow the data to grow as much as it can. This is a great fit for the Composite Design Pattern in object composition.
Here are some other clear use cases:
- A shopping cart contains individual items. It also has bundles of items sold together. Both need a price.
- A tax system has individual taxpayers. It also has family groups and corporate groups. All of them need tax calculated, discounts applied, and year-to-date totals computed.
- A file system has individual files. It also has folders that contain files or other folders. Both need a size.
The naïve approach is to write separate logic for individuals and groups, then add a type check wherever you need to handle both. But the logic diverges. The type checks multiply. And every new operation means updating both branches. The code becomes harder to extend and harder to trust.
The Composite Design Pattern eliminates this entirely. It defines a common interface that both individual objects and groups implement. The calling code never checks types. It calls the same method on a leaf or a composite and gets the correct result either way.
Table of Contents
- Prerequisites
- What is the Composite Design Pattern?
- The Three Layers
- Real World Example One: Shopping Cart Pricing
- Real World Example Two: Tax Management
- The Power of Nested Composites
- The Composite Pattern in C#
- When to Use the Composite Pattern
- When Not to Use It
- Conclusion
Prerequisites
Before reading this article, you should be comfortable with:
- Object-oriented programming: abstract classes, interfaces, and inheritance
- What a design pattern is at a conceptual level
- Basic Dart or C# syntax
You don’t need prior experience with structural design patterns. This article introduces the Composite pattern from first principles with real production examples.
What is the Composite Design Pattern?
The Composite Design Pattern is a structural design pattern. Where creational patterns deal with how objects are created and behavioral patterns deal with how objects communicate, structural patterns deal with how objects are composed and related to each other.
The Composite pattern specifically deals with part-whole hierarchies. It lets you compose objects into tree structures and then work with those trees as if every node in the tree is the same type of thing.
The core idea is deceptively simple: define a common interface, make individual objects implement it, and make groups of objects implement it too. Now everything in the hierarchy responds to the same methods and the calling code never needs to distinguish between a leaf and a composite.
This is what “treating individual objects and groups through a unified interface” means in practice. One method call, any object in the hierarchy, correct result — regardless of whether you are calling it on a single item or a nested group containing hundreds of items.
The Three Layers
The Composite pattern has three distinct layers. Understanding each one before looking at code makes the implementation much clearer.
The Component Layer
This is the abstract class or interface that defines the contract for every object in the hierarchy. It declares the methods that both individual objects and groups must implement.
The Component is what makes uniform treatment possible: because everything in the hierarchy implements this interface, everything responds to the same method calls.
The Leaf Layer
A Leaf is a concrete implementation of the Component. It represents an individual object with no children — like a single item in a shopping cart, a single taxpayer, or a single file. The Leaf implements the Component methods with its own specific logic.
The Composite Layer
A Composite is also a concrete implementation of the Component. But unlike a Leaf, it holds a collection of children. Each child is a Component, which means each child can be either a Leaf or another Composite.
The Composite implements the Component methods by delegating to its children and aggregating the results.
The relationship between these layers is what enables the tree structure and the uniform interface simultaneously.
Real World Example One: Shopping Cart Pricing
A shopping cart needs to calculate prices. Individual items have their own prices. Bundles group multiple items and their price is the sum of their contents. Both need to respond to getPrice().
The Component
abstract class PriceComponent {
double getPrice();
}
PriceComponent is the contract. Every object in the cart hierarchy must implement getPrice(). That is the entire interface: one method that is uniform across all objects.
The Leaf
class CartItem extends PriceComponent {
final int id;
final String name;
final double price;
CartItem({required this.id, required this.name, required this.price});
@override
double getPrice() {
return price;
}
}
CartItem is the Leaf. It represents a single item in the cart. Its getPrice() returns its own price directly. There’s no delegation or children — just its own value.
The Composite
class ItemBundle extends PriceComponent {
final int bundleId;
final String bundleName;
final List<PriceComponent> _items = [];
ItemBundle({required this.bundleId, required this.bundleName});
void add(PriceComponent component) {
_items.add(component);
}
void remove(PriceComponent component) {
_items.remove(component);
}
@override
double getPrice() {
return _items.fold(0, (total, item) => total + item.getPrice());
}
}
ItemBundle is the Composite. It holds a list of PriceComponent children. Its getPrice() delegates to its children using fold, summing up whatever each child returns.
The critical detail: _items is a List<PriceComponent>, not a List<CartItem>. This means an ItemBundle can contain both CartItem leaves and other ItemBundle composites. The hierarchy can nest as deeply as needed.
Using It
void main() {
final burger = CartItem(id: 1, name: 'Burger', price: 5.99);
final fries = CartItem(id: 2, name: 'Fries', price: 2.99);
final drink = CartItem(id: 3, name: 'Drink', price: 1.99);
final apple = CartItem(id: 4, name: 'Apple', price: 0.99);
final comboMeal = ItemBundle(bundleId: 1, bundleName: 'Combo Meal');
comboMeal
..add(burger)
..add(fries)
..add(drink);
final cart = ItemBundle(bundleId: 0, bundleName: 'My Cart');
cart
..add(comboMeal)
..add(apple);
// same method call on everything
print('Burger: \$${burger.getPrice()}');
print('Combo Meal: \$${comboMeal.getPrice()}');
print('Full Cart: \$${cart.getPrice()}');
}
burger.getPrice() calls the Leaf implementation directly. comboMeal.getPrice() calls the Composite implementation, which delegates to its three children. cart.getPrice() calls the Composite implementation, which delegates to the combo meal composite and the apple leaf.
The calling code treats all of them identically: getPrice(), result, done.
Real World Example Two: Tax Management
This example shows the Composite pattern applied to a more complex domain. A tax management system needs to calculate tax amounts, apply discounts, and compute year-to-date totals. These calculations need to work for individual taxpayers and for groups of taxpayers through exactly the same interface.
The Component
abstract class TaxManager {
num getTaxAmount();
num getTaxDiscount();
num getTotalTaxYTD();
}
TaxManager defines three methods. Every object in the tax hierarchy must implement all three. A single taxpayer implements them with their own data. A group implements them by aggregating across all members. The calling code calls any of these methods on any object and gets the correct result.
The Leaf
class SingleUser extends TaxManager {
final num _amount;
final List<num> _allTaxes;
SingleUser(this._amount, this._allTaxes);
@override
num getTaxAmount() {
return _amount;
}
@override
num getTaxDiscount() {
return _amount % 2 == 0 ? _amount : (_amount / 2);
}
@override
num getTotalTaxYTD() {
num total = 0;
for (final tax in _allTaxes) {
total += tax;
}
return total;
}
}
SingleUser is the Leaf. It represents one individual taxpayer. _amount is their current tax amount. _allTaxes is a list of their tax payments over the year. Each method operates on this person’s data only.
getTaxDiscount() applies a simple discount rule: even amounts receive the full amount, odd amounts receive half. This rule lives on the individual and is automatically propagated through any group that contains this user, because the Composite delegates to each child’s own implementation.
The Composite
class TaxGroup extends TaxManager {
final List<TaxManager> _members = [];
void add(TaxManager member) {
_members.add(member);
}
void remove(TaxManager member) {
_members.remove(member);
}
@override
num getTaxAmount() {
return _members.fold(0, (total, m) => total + m.getTaxAmount());
}
@override
num getTaxDiscount() {
return _members.fold(0, (total, m) => total + m.getTaxDiscount());
}
@override
num getTotalTaxYTD() {
return _members.fold(0, (total, m) => total + m.getTotalTaxYTD());
}
}
TaxGroup is the Composite. It holds a list of TaxManager children and delegates each method call across all of them, aggregating the results. A TaxGroup can contain SingleUser leaves or other TaxGroup composites, enabling arbitrarily deep nesting.
Using It
void main() {
final alice = SingleUser(120, [100, 110, 120]);
final bob = SingleUser(95, [80, 90, 95]);
final carol = SingleUser(200, [150, 175, 200]);
final family = TaxGroup();
family..add(alice)..add(bob);
final corporate = TaxGroup();
corporate..add(family)..add(carol);
print('Alice tax: ${alice.getTaxAmount()}');
print('Family total tax: ${family.getTaxAmount()}');
print('Corporate total tax: ${corporate.getTaxAmount()}');
print('Corporate YTD: ${corporate.getTotalTaxYTD()}');
}
Every call uses the same three methods regardless of whether the target is an individual or a group. The Composite handles aggregation internally — the calling code never needs to know.
The Power of Nested Composites
Both examples demonstrate that a Composite can contain other Composites. This is the property that makes the pattern so powerful for real-world hierarchies.
A shopping cart bundle can contain other bundles. A tax group can contain other tax groups. A file folder can contain other folders. The hierarchy can be as flat or as deep as the domain requires, and the calling code never changes. It always calls the same method on a PriceComponent or a TaxManager and gets the correct result.
This is the tree structure the pattern is named for. The Component interface is the root concept. Leaves are the terminal nodes. Composites are the internal nodes, and any internal node can itself be a child of another internal node.
The Composite Pattern in C#
The same pattern translates directly to C#. Here is the shopping cart example using a C# interface and classes.
The Component
public interface IPriceComponent
{
double GetPrice();
}
The Leaf
public class CartItem : IPriceComponent
{
public int Id { get; }
public string Name { get; }
private readonly double _price;
public CartItem(int id, string name, double price)
{
Id = id;
Name = name;
_price = price;
}
public double GetPrice() => _price;
}
The Composite
public class ItemBundle : IPriceComponent
{
public int BundleId { get; }
public string BundleName { get; }
private readonly List<IPriceComponent> _items = new();
public ItemBundle(int bundleId, string bundleName)
{
BundleId = bundleId;
BundleName = bundleName;
}
public void Add(IPriceComponent component) => _items.Add(component);
public void Remove(IPriceComponent component) => _items.Remove(component);
public double GetPrice() => _items.Sum(item => item.GetPrice());
}
Using It
var burger = new CartItem(1, "Burger", 5.99);
var fries = new CartItem(2, "Fries", 2.99);
var drink = new CartItem(3, "Drink", 1.99);
var apple = new CartItem(4, "Apple", 0.99);
var comboMeal = new ItemBundle(1, "Combo Meal");
comboMeal.Add(burger);
comboMeal.Add(fries);
comboMeal.Add(drink);
var cart = new ItemBundle(0, "My Cart");
cart.Add(comboMeal);
cart.Add(apple);
Console.WriteLine($"Burger: ${burger.GetPrice()}");
Console.WriteLine($"Combo Meal: ${comboMeal.GetPrice()}");
Console.WriteLine($"Full Cart: ${cart.GetPrice()}");
The structure is identical to the Dart version. The interface replaces the abstract class, GetPrice() replaces getPrice(), and LINQ’s Sum replaces fold. The pattern itself does not change between languages.
When to Use the Composite Pattern
Use the Composite pattern when:
- Your domain has a natural part-whole hierarchy — items and bundles, individuals and groups, files and folders.
- You want calling code to treat leaves and composites identically without type checks.
- You need the hierarchy to be open to extension: new leaf types or new composite types should not require changes to calling code.
- Operations need to propagate through the entire tree without the caller managing that propagation manually.
When Not to Use It
Avoid the Composite pattern when:
- Your hierarchy is genuinely flat and will not grow. Adding the pattern to a flat list of items is unnecessary abstraction.
- Leaves and composites need significantly different interfaces. If the calling code always needs to distinguish between them, the pattern is working against you rather than for you.
- Performance is critical at extreme scale and the recursive delegation through a deep tree is a measured bottleneck. In most applications this is not an issue, but it is worth considering in tight loops over very large hierarchies.
Conclusion
The Composite Design Pattern solves a specific and recurring problem: how to treat individual objects and groups of objects through the same interface without type checks or duplicated logic. It does this by defining a common Component interface, implementing it in Leaf classes for individual objects, and implementing it in Composite classes that delegate to their children.
The result is a tree structure where every node — whether a terminal leaf or an internal composite — responds to the same methods. Calling code stays simple and stable. The hierarchy can grow in depth and breadth without requiring any changes to the code that uses it. When your domain has natural part-whole relationships, the Composite pattern is one of the clearest tools available for keeping that complexity under control.