The permission system was straightforward. Each user had a list of permissions. Check if the user has CanEditOrders? Look it up in the list.
Then someone added roles. A role is a collection of permissions. Now checking CanEditOrders means checking the user's direct permissions AND iterating through all their roles.
Then roles got nested. An "Admin" role includes the "Manager" role, which includes the "Editor" role. Now your permission check is three levels deep with different logic at each level.
The checking code became a nightmare of type-checking and special cases:
public bool HasPermission(User user, string permission){ // Check direct permissions if (user.Permissions.Contains(permission)) return true; // Check roles foreach (var role in user.Roles) { if (role.Permissions.Contains(permission)) return true; // Check nested roles foreach (var nestedRole in role.ChildRoles) { if (nestedRole.Permissions.Contains(permission)) return true; // What about roles nested 3 levels deep? // 4 levels? This doesn't scale. } } return false;}
Every new level of nesting requires another loop. And if the structure changes? Every consumer breaks.
The core issue is that you're treating single permissions and groups of permissions as fundamentally different things. But from the caller's perspective, the question is always the same: "does this grant access?"
It shouldn't matter whether the answer comes from a single permission, a role, or a deeply nested role hierarchy.
The Composite pattern lets you compose objects into tree structures and then work with these trees as if they were individual objects. Every node — leaf or container — implements the same interface.
Define a unified interface for permissions:
// Common interface for all permission nodespublic interface IPermissionComponent{ string Name { get; } bool HasPermission(string permission); IEnumerable<IPermissionComponent> GetChildren();} // Leaf: a single permissionpublic class Permission : IPermissionComponent{ public string Name { get; } public Permission(string name) => Name = name; public bool HasPermission(string permission) => Name.Equals(permission, StringComparison.OrdinalIgnoreCase); public IEnumerable<IPermissionComponent> GetChildren() => Enumerable.Empty<IPermissionComponent>();} // Composite: a group of permissions (a role)public class PermissionGroup : IPermissionComponent{ public string Name { get; } private readonly List<IPermissionComponent> _children = new(); public PermissionGroup(string name) => Name = name; public void Add(IPermissionComponent component) => _children.Add(component); public void Remove(IPermissionComponent component) => _children.Remove(component); public bool HasPermission(string permission) { // Recursively check all children — whether they're // individual permissions or nested groups return _children.Any(c => c.HasPermission(permission)); } public IEnumerable<IPermissionComponent> GetChildren() => _children;}
Now build permission hierarchies naturally:
// Define individual permissionsvar canRead = new Permission("CanReadOrders");var canEdit = new Permission("CanEditOrders");var canDelete = new Permission("CanDeleteOrders");var canManageUsers = new Permission("CanManageUsers");var canViewReports = new Permission("CanViewReports"); // Build role hierarchyvar editorRole = new PermissionGroup("Editor");editorRole.Add(canRead);editorRole.Add(canEdit); var managerRole = new PermissionGroup("Manager");managerRole.Add(editorRole); // Manager includes EditormanagerRole.Add(canViewReports); var adminRole = new PermissionGroup("Admin");adminRole.Add(managerRole); // Admin includes ManageradminRole.Add(canDelete);adminRole.Add(canManageUsers); // Check permission - works the same regardless of depthbool canAdminEdit = adminRole.HasPermission("CanEditOrders"); // true (3 levels deep)bool canEditorDelete = editorRole.HasPermission("CanDeleteOrders"); // false
The caller doesn't care whether it's checking a single permission or a 5-level-deep role hierarchy. Same method, same interface.
Uniform treatment. Clients call HasPermission() on any node. No type-checking, no special traversal logic.
Unlimited nesting. Add as many levels as your business requires. The recursive structure handles it automatically.
Easy to extend. Add a ConditionalPermission that checks time of day, or a FeatureFlagPermission that checks a flag store. As long as it implements IPermissionComponent, it plugs right in.
E-commerce pricing is another perfect fit. Discounts can be flat, percentage-based, or combinations:
public interface IPricingRule{ decimal CalculateDiscount(decimal originalPrice, OrderContext context);} // Leaf: single discountpublic class PercentageDiscount : IPricingRule{ private readonly decimal _percentage; public PercentageDiscount(decimal percentage) => _percentage = percentage; public decimal CalculateDiscount(decimal originalPrice, OrderContext context) => originalPrice * (_percentage / 100m);} public class FlatDiscount : IPricingRule{ private readonly decimal _amount; public FlatDiscount(decimal amount) => _amount = amount; public decimal CalculateDiscount(decimal originalPrice, OrderContext context) => Math.Min(_amount, originalPrice);} // Composite: stacked discountspublic class DiscountBundle : IPricingRule{ private readonly List<IPricingRule> _rules = new(); public void Add(IPricingRule rule) => _rules.Add(rule); public decimal CalculateDiscount(decimal originalPrice, OrderContext context) { // Apply discounts sequentially var totalDiscount = 0m; var currentPrice = originalPrice; foreach (var rule in _rules) { var discount = rule.CalculateDiscount(currentPrice, context); totalDiscount += discount; currentPrice -= discount; } return totalDiscount; }} // Build a Black Friday deal: 20% off + $10 couponvar blackFriday = new DiscountBundle();blackFriday.Add(new PercentageDiscount(20));blackFriday.Add(new FlatDiscount(10)); var totalDiscount = blackFriday.CalculateDiscount(100m, context); // $30
Dynamic navigation menus are tree structures by nature:
public interface IMenuItem{ string Label { get; } string? Url { get; } bool IsVisible(UserContext user); IReadOnlyList<IMenuItem> Children { get; }} public class MenuLink : IMenuItem{ public string Label { get; } public string? Url { get; } private readonly string? _requiredPermission; public MenuLink(string label, string url, string? requiredPermission = null) { Label = label; Url = url; _requiredPermission = requiredPermission; } public bool IsVisible(UserContext user) => _requiredPermission == null || user.HasPermission(_requiredPermission); public IReadOnlyList<IMenuItem> Children => Array.Empty<IMenuItem>();} public class MenuGroup : IMenuItem{ public string Label { get; } public string? Url => null; private readonly List<IMenuItem> _children = new(); public MenuGroup(string label) => Label = label; public void Add(IMenuItem item) => _children.Add(item); public bool IsVisible(UserContext user) => _children.Any(c => c.IsVisible(user)); public IReadOnlyList<IMenuItem> Children => _children;}
When your structure is always flat. If you never have nesting, the Composite adds complexity for no gain. A simple list is enough.
When leaf and composite behavior differ significantly. If operations on groups are fundamentally different from operations on individuals, forcing a common interface creates awkward implementations.
When you need parent references. The basic Composite doesn't track parents. If you frequently need "go up the tree," you'll need extra wiring that complicates the pattern.
The Composite pattern organizes objects into tree structures where every node — whether it's a single item or a group — implements the same interface. This lets clients treat individual objects and compositions of objects identically.
Use it when your data has a natural tree structure (permissions, menus, folders, org charts) and you want uniform operations across the entire tree. If you catch yourself writing recursive type-checking code, Composite is likely the answer.
For flat collections with no nesting, yes. If your data is always a simple list, use a list. Composite adds value when nesting depth varies or when you need to add/remove levels without changing client code.
For simple hierarchies, recursive methods on a single class can work. The Visitor pattern pairs well with Composite when you need many different operations on the same tree. For flat structures, standard LINQ on collections is simpler.
The Composite pattern shows up everywhere once you start looking. File systems, UI component trees, permission hierarchies, organizational charts, pricing engines — they're all trees where you need uniform behavior across nodes.
The pattern's power is in its simplicity. One interface. Leaves implement it directly. Composites delegate to their children. Clients never know the difference.
That's all from me today.
P.S. Follow me on YouTube.
If you made it this far, you're clearly serious about writing better .NET code. Here's a 20% discount code: DEEP20 for Design Patterns that Deliver. Consider it a thank-you for actually reading the whole thing.
Here are 2 ebooks I have about design patterns:
Design Patterns that Deliver — 5 essential patterns (Builder, Decorator, Strategy, Adapter, Mediator) with production-ready C# code and real-world examples. Or try a free chapter on the Builder Pattern first.
Design Patterns Simplified — A beginner-friendly guide to understanding design patterns without the academic fluff.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.