BrandGhost
Composite Pattern Real-World Example in C#: Complete Implementation

Composite Pattern Real-World Example in C#: Complete Implementation

Composite Pattern Real-World Example in C#: Complete Implementation

Most composite pattern tutorials model a file system with "File" and "Folder" classes, print a tree to the console, and call it a day. That's fine for understanding the concept, but it falls apart the moment you need to answer a real business question like "does this department have permission to access the billing dashboard?" This article builds a complete composite pattern real-world example in C# from scratch: an organizational permission system where permissions cascade through a hierarchy of users, teams, departments, and the organization itself.

By the end, you'll have compilable classes that you can drop into a .NET project. We'll design the component interface, build a leaf class for individual users, create composite classes for teams, departments, and the organization, wire the hierarchy together with dependency injection, and write tests that verify recursive permission resolution at every level. If you've been hunting for a practical composite pattern implementation that solves a genuine business problem, this is it.

The Problem: Permission Checks Without the Composite Pattern

Consider a system that needs to determine whether a user, team, or department has access to a specific resource. Without the composite pattern, the permission-checking logic degenerates into a brittle chain of type checks and special cases:

public class PermissionChecker
{
    public bool CanAccess(
        string resource,
        string entityType,
        string entityId,
        Dictionary<string, HashSet<string>> userPermissions,
        Dictionary<string, List<string>> teamMembers,
        Dictionary<string, List<string>> departmentTeams,
        Dictionary<string, HashSet<string>> teamPermissions,
        Dictionary<string, HashSet<string>> departmentPermissions)
    {
        if (entityType == "user")
        {
            return userPermissions
                .GetValueOrDefault(entityId)?
                .Contains(resource) ?? false;
        }
        else if (entityType == "team")
        {
            if (teamPermissions
                .GetValueOrDefault(entityId)?
                .Contains(resource) ?? false)
            {
                return true;
            }

            var members = teamMembers
                .GetValueOrDefault(entityId);
            if (members is null)
            {
                return false;
            }

            foreach (var member in members)
            {
                if (userPermissions
                    .GetValueOrDefault(member)?
                    .Contains(resource) ?? false)
                {
                    return true;
                }
            }

            return false;
        }
        else if (entityType == "department")
        {
            if (departmentPermissions
                .GetValueOrDefault(entityId)?
                .Contains(resource) ?? false)
            {
                return true;
            }

            var teams = departmentTeams
                .GetValueOrDefault(entityId);
            if (teams is null)
            {
                return false;
            }

            foreach (var team in teams)
            {
                if (CanAccess(
                    resource, "team", team,
                    userPermissions, teamMembers,
                    departmentTeams, teamPermissions,
                    departmentPermissions))
                {
                    return true;
                }
            }

            return false;
        }

        return false;
    }
}

This approach has serious problems. The CanAccess method needs to know every entity type and how each one stores its members and permissions. Adding a new level -- say, a "division" between department and organization -- means rewriting this method and every caller that passes data into it. The method signature alone is a maintenance nightmare with seven parameters. Testing requires constructing parallel dictionaries that mirror the hierarchy, and there's no way to reuse the traversal logic for related operations like "give me all permissions this entity has."

The composite pattern eliminates all of this by giving every node in the hierarchy -- whether it's a single user or an entire organization -- the same interface for permission queries.

Designing the Permission Component Interface

The core of every composite pattern real-world example in C# is a shared interface that both leaf and composite nodes implement. For our permission system, this interface needs three operations: checking whether a specific permission exists anywhere in the subtree, returning the entity's name for display, and aggregating all permissions across the hierarchy:

using System.Collections.Generic;

namespace OrgPermissions.Core;

public interface IPermissionComponent
{
    string GetName();

    bool HasPermission(string permission);

    IReadOnlyCollection<string> GetAllPermissions();
}

The interface is intentionally small. HasPermission answers a yes-or-no question that can short-circuit as soon as it finds a match anywhere in the tree. GetAllPermissions returns the full set of permissions, which is useful for auditing and UI display. Both operations work identically whether you're calling them on a single user or on an organization with hundreds of nested components -- that's the entire point of the composite pattern.

Notice that GetAllPermissions returns IReadOnlyCollection<string> rather than IEnumerable<string>. This is deliberate. Callers often need to check the count or iterate multiple times, and returning a materialized collection avoids the pitfalls of deferred execution on a recursive aggregation. If you've explored how composition in C# brings objects together to form larger structures, the composite pattern takes that idea and formalizes it into a recursive tree.

Building the User (Leaf)

The User class is our leaf node. It holds a set of directly assigned permissions and implements the component interface without delegating to any children:

using System.Collections.Generic;
using System.Linq;

namespace OrgPermissions.Core;

public sealed class User : IPermissionComponent
{
    private readonly string _name;
    private readonly HashSet<string> _permissions;

    public User(
        string name,
        IEnumerable<string> permissions)
    {
        _name = name;
        _permissions = new HashSet<string>(
            permissions,
            StringComparer.OrdinalIgnoreCase);
    }

    public User(string name)
        : this(name, Enumerable.Empty<string>())
    {
    }

    public string GetName() => _name;

    public bool HasPermission(string permission) =>
        _permissions.Contains(permission);

    public IReadOnlyCollection<string> GetAllPermissions() =>
        _permissions;

    public void GrantPermission(string permission) =>
        _permissions.Add(permission);

    public void RevokePermission(string permission) =>
        _permissions.Remove(permission);
}

A few design decisions are worth calling out. The HashSet<string> gives us O(1) permission lookups, which matters when a user might have dozens of permissions being checked in a hot path. The StringComparer.OrdinalIgnoreCase ensures that "billing.read" and "Billing.Read" are treated as the same permission -- a common source of bugs in case-sensitive permission systems.

The GrantPermission and RevokePermission methods exist only on the User class, not on IPermissionComponent. This is intentional. Permission assignment is a leaf concern. Composites aggregate and query permissions from their children; they don't own permissions directly. Keeping mutation off the shared interface means composite nodes stay focused on delegation and traversal.

Building the Team (Composite)

The Team class is our first composite. It holds a collection of IPermissionComponent children -- which can be individual users or nested sub-teams -- and delegates permission queries recursively:

using System;
using System.Collections.Generic;
using System.Linq;

namespace OrgPermissions.Core;

public sealed class Team : IPermissionComponent
{
    private readonly string _name;
    private readonly List<IPermissionComponent> _members = new();

    public Team(string name)
    {
        _name = name;
    }

    public string GetName() => _name;

    public void Add(IPermissionComponent member)
    {
        if (member is null)
        {
            throw new ArgumentNullException(nameof(member));
        }

        _members.Add(member);
    }

    public void Remove(IPermissionComponent member) =>
        _members.Remove(member);

    public IReadOnlyList<IPermissionComponent> GetMembers() =>
        _members.AsReadOnly();

    public bool HasPermission(string permission) =>
        _members.Any(m => m.HasPermission(permission));

    public IReadOnlyCollection<string> GetAllPermissions()
    {
        var allPermissions = new HashSet<string>(
            StringComparer.OrdinalIgnoreCase);

        foreach (var member in _members)
        {
            foreach (var permission in
                member.GetAllPermissions())
            {
                allPermissions.Add(permission);
            }
        }

        return allPermissions;
    }
}

The HasPermission method uses LINQ's Any to short-circuit the search as soon as any member -- whether a user or nested sub-team -- reports that it has the requested permission. This is important for performance. If the first user in a fifty-person team has the permission, we don't need to check the other forty-nine.

GetAllPermissions takes the opposite approach and exhaustively traverses every child, collecting all unique permissions into a HashSet<string>. The case-insensitive comparer ensures deduplication works correctly when the same permission appears at multiple levels of the hierarchy. Because each child's GetAllPermissions call might itself recurse into further children, this single method handles arbitrarily deep nesting without any special traversal logic. That recursive delegation is what makes the composite pattern so powerful -- the Team class doesn't need to know whether its members are users, sub-teams, or any future node type.

Building the Department and Organization

Higher-level composites follow the same structure. A Department contains teams and individual users. An Organization contains departments, standalone teams, and direct members. Both reuse the identical recursive pattern:

using System;
using System.Collections.Generic;
using System.Linq;

namespace OrgPermissions.Core;

public sealed class Department : IPermissionComponent
{
    private readonly string _name;
    private readonly List<IPermissionComponent> _children = new();

    public Department(string name)
    {
        _name = name;
    }

    public string GetName() => _name;

    public void Add(IPermissionComponent child)
    {
        if (child is null)
        {
            throw new ArgumentNullException(nameof(child));
        }

        _children.Add(child);
    }

    public void Remove(IPermissionComponent child) =>
        _children.Remove(child);

    public IReadOnlyList<IPermissionComponent> GetChildren() =>
        _children.AsReadOnly();

    public bool HasPermission(string permission) =>
        _children.Any(c => c.HasPermission(permission));

    public IReadOnlyCollection<string> GetAllPermissions()
    {
        var allPermissions = new HashSet<string>(
            StringComparer.OrdinalIgnoreCase);

        foreach (var child in _children)
        {
            foreach (var permission in
                child.GetAllPermissions())
            {
                allPermissions.Add(permission);
            }
        }

        return allPermissions;
    }
}
using System;
using System.Collections.Generic;
using System.Linq;

namespace OrgPermissions.Core;

public sealed class Organization : IPermissionComponent
{
    private readonly string _name;
    private readonly List<IPermissionComponent> _units = new();

    public Organization(string name)
    {
        _name = name;
    }

    public string GetName() => _name;

    public void Add(IPermissionComponent unit)
    {
        if (unit is null)
        {
            throw new ArgumentNullException(nameof(unit));
        }

        _units.Add(unit);
    }

    public void Remove(IPermissionComponent unit) =>
        _units.Remove(unit);

    public IReadOnlyList<IPermissionComponent> GetUnits() =>
        _units.AsReadOnly();

    public bool HasPermission(string permission) =>
        _units.Any(u => u.HasPermission(permission));

    public IReadOnlyCollection<string> GetAllPermissions()
    {
        var allPermissions = new HashSet<string>(
            StringComparer.OrdinalIgnoreCase);

        foreach (var unit in _units)
        {
            foreach (var permission in
                unit.GetAllPermissions())
            {
                allPermissions.Add(permission);
            }
        }

        return allPermissions;
    }
}

You might notice that Department and Organization look very similar to Team. That's not a coincidence -- it's the composite pattern working exactly as intended. Every composite node has the same core responsibility: hold children and delegate queries. The class names and collection field names differ because they represent different domain concepts, but the recursive mechanics are identical.

In a production system, you might factor the shared traversal logic into a base class or use a generic composite. For this article, keeping each class explicit makes the pattern easier to follow and avoids introducing inheritance hierarchies that obscure the point. The structural patterns like composite and decorator share this philosophy of keeping each class focused on a single level of the tree.

Recursive Permission Resolution

The real power of this composite pattern real-world example in C# is what happens when you call HasPermission or GetAllPermissions on a deeply nested hierarchy. Let's trace through both operations to see how the recursion unfolds.

Consider this organizational structure:

Acme Corp (Organization)
├── Engineering (Department)
│   ├── Backend Team (Team)
│   │   ├── Alice (User: api.read, api.write)
│   │   └── Bob (User: api.read, db.admin)
│   └── Frontend Team (Team)
│       └── Carol (User: ui.deploy, api.read)
└── Finance (Department)
    └── Dave (User: billing.read, billing.write)

When you call organization.HasPermission("db.admin"), the call cascades like this: the Organization asks each of its children. Engineering asks its children. Backend Team asks its members. Alice says no. Bob says yes. The Any call short-circuits, and the result propagates back up through the entire chain. Frontend Team and Finance are never checked because the answer was already found.

When you call organization.GetAllPermissions(), every node is visited. The Organization iterates over Engineering and Finance. Each department iterates over its teams. Each team iterates over its users. Every user returns its HashSet<string>, and the results merge upward through the HashSet deduplication at each composite level. The final result is a single collection containing all unique permissions across the entire organization: api.read, api.write, db.admin, ui.deploy, billing.read, and billing.write.

This behavior is what separates the composite pattern from a simple container. The caller doesn't need to know the shape of the tree, how many levels deep it goes, or which nodes are leaves and which are composites. The same HasPermission("db.admin") call works identically whether you pass it a single User, a Team, or the entire Organization. That uniform treatment is especially valuable when you're building systems like plugin architectures where components need to be assembled and queried without knowing their internal structure.

Wiring It Together with Dependency Injection

In a real application, you'd build the organizational tree from a database or configuration system and register the root component with the DI container. Here's how to wire up the permission hierarchy using IServiceCollection:

using Microsoft.Extensions.DependencyInjection;

using OrgPermissions.Core;

namespace OrgPermissions;

public static class PermissionServiceRegistration
{
    public static IServiceCollection AddPermissionSystem(
        this IServiceCollection services)
    {
        services.AddSingleton<IPermissionComponent>(
            _ => BuildOrganization());

        return services;
    }

    private static IPermissionComponent BuildOrganization()
    {
        var alice = new User(
            "Alice",
            new[] { "api.read", "api.write" });
        var bob = new User(
            "Bob",
            new[] { "api.read", "db.admin" });
        var carol = new User(
            "Carol",
            new[] { "ui.deploy", "api.read" });
        var dave = new User(
            "Dave",
            new[] { "billing.read", "billing.write" });

        var backendTeam = new Team("Backend Team");
        backendTeam.Add(alice);
        backendTeam.Add(bob);

        var frontendTeam = new Team("Frontend Team");
        frontendTeam.Add(carol);

        var engineering = new Department("Engineering");
        engineering.Add(backendTeam);
        engineering.Add(frontendTeam);

        var finance = new Department("Finance");
        finance.Add(dave);

        var organization = new Organization("Acme Corp");
        organization.Add(engineering);
        organization.Add(finance);

        return organization;
    }
}

The key insight is that the DI container registers the root IPermissionComponent as a singleton. Any service that needs to check permissions depends on IPermissionComponent -- it doesn't know or care whether it received a User, a Team, or an Organization. This is the composite pattern's greatest strength for DI: the consuming code is completely decoupled from the tree structure.

You could also register a factory method that loads the hierarchy from a database at startup, or use a scoped registration that rebuilds the tree per request for multi-tenant scenarios. The composite pattern doesn't dictate how the tree is constructed -- only that every node in it satisfies the same interface. If you want to simplify the construction process behind a single entry point, the facade pattern pairs naturally with composite for exposing a clean API over a complex hierarchy.

Here's a simple service that consumes the permission tree:

using OrgPermissions.Core;

namespace OrgPermissions;

public sealed class PermissionService
{
    private readonly IPermissionComponent _root;

    public PermissionService(IPermissionComponent root)
    {
        _root = root;
    }

    public bool IsAuthorized(string permission) =>
        _root.HasPermission(permission);

    public IReadOnlyCollection<string> GetAllGrantedPermissions() =>
        _root.GetAllPermissions();
}

The PermissionService doesn't know whether _root is a single user or an entire organizational tree. It calls the same methods either way, and the composite pattern handles the recursive resolution transparently.

Testing the Permission System

Testing a composite hierarchy requires verifying behavior at every level: individual leaves, composites with direct children, and deep trees with multiple nesting levels. Here's a comprehensive test suite:

using System.Linq;

using OrgPermissions.Core;

using Xunit;

namespace OrgPermissions.Tests;

public class UserTests
{
    [Fact]
    public void HasPermission_PermissionExists_ReturnsTrue()
    {
        var user = new User(
            "Alice",
            new[] { "api.read", "api.write" });

        Assert.True(user.HasPermission("api.read"));
    }

    [Fact]
    public void HasPermission_PermissionMissing_ReturnsFalse()
    {
        var user = new User(
            "Alice",
            new[] { "api.read" });

        Assert.False(user.HasPermission("db.admin"));
    }

    [Fact]
    public void HasPermission_CaseInsensitive_ReturnsTrue()
    {
        var user = new User(
            "Alice",
            new[] { "Api.Read" });

        Assert.True(user.HasPermission("api.read"));
    }

    [Fact]
    public void GetAllPermissions_ReturnsAllAssigned()
    {
        var user = new User(
            "Alice",
            new[] { "api.read", "api.write" });

        var permissions = user.GetAllPermissions();

        Assert.Equal(2, permissions.Count);
        Assert.Contains("api.read", permissions);
        Assert.Contains("api.write", permissions);
    }

    [Fact]
    public void GrantPermission_NewPermission_IsAccessible()
    {
        var user = new User("Alice");

        user.GrantPermission("api.read");

        Assert.True(user.HasPermission("api.read"));
    }

    [Fact]
    public void RevokePermission_ExistingPermission_IsRemoved()
    {
        var user = new User(
            "Alice",
            new[] { "api.read" });

        user.RevokePermission("api.read");

        Assert.False(user.HasPermission("api.read"));
    }
}
using System.Linq;

using OrgPermissions.Core;

using Xunit;

namespace OrgPermissions.Tests;

public class TeamTests
{
    [Fact]
    public void HasPermission_MemberHasIt_ReturnsTrue()
    {
        var team = new Team("Backend");
        team.Add(new User(
            "Alice",
            new[] { "api.read" }));
        team.Add(new User(
            "Bob",
            new[] { "db.admin" }));

        Assert.True(team.HasPermission("db.admin"));
    }

    [Fact]
    public void HasPermission_NoMemberHasIt_ReturnsFalse()
    {
        var team = new Team("Backend");
        team.Add(new User(
            "Alice",
            new[] { "api.read" }));

        Assert.False(team.HasPermission("billing.read"));
    }

    [Fact]
    public void HasPermission_NestedSubTeam_ReturnsTrue()
    {
        var subTeam = new Team("API Sub-Team");
        subTeam.Add(new User(
            "Alice",
            new[] { "api.write" }));

        var parentTeam = new Team("Backend");
        parentTeam.Add(subTeam);

        Assert.True(parentTeam.HasPermission("api.write"));
    }

    [Fact]
    public void GetAllPermissions_AggregatesFromAllMembers()
    {
        var team = new Team("Backend");
        team.Add(new User(
            "Alice",
            new[] { "api.read", "api.write" }));
        team.Add(new User(
            "Bob",
            new[] { "api.read", "db.admin" }));

        var permissions = team.GetAllPermissions();

        Assert.Equal(3, permissions.Count);
        Assert.Contains("api.read", permissions);
        Assert.Contains("api.write", permissions);
        Assert.Contains("db.admin", permissions);
    }

    [Fact]
    public void GetAllPermissions_EmptyTeam_ReturnsEmpty()
    {
        var team = new Team("Empty");

        var permissions = team.GetAllPermissions();

        Assert.Empty(permissions);
    }
}
using System.Linq;

using OrgPermissions.Core;

using Xunit;

namespace OrgPermissions.Tests;

public class OrganizationHierarchyTests
{
    private readonly Organization _organization;

    public OrganizationHierarchyTests()
    {
        var alice = new User(
            "Alice",
            new[] { "api.read", "api.write" });
        var bob = new User(
            "Bob",
            new[] { "api.read", "db.admin" });
        var carol = new User(
            "Carol",
            new[] { "ui.deploy", "api.read" });
        var dave = new User(
            "Dave",
            new[] { "billing.read", "billing.write" });

        var backendTeam = new Team("Backend Team");
        backendTeam.Add(alice);
        backendTeam.Add(bob);

        var frontendTeam = new Team("Frontend Team");
        frontendTeam.Add(carol);

        var engineering = new Department("Engineering");
        engineering.Add(backendTeam);
        engineering.Add(frontendTeam);

        var finance = new Department("Finance");
        finance.Add(dave);

        _organization = new Organization("Acme Corp");
        _organization.Add(engineering);
        _organization.Add(finance);
    }

    [Fact]
    public void HasPermission_DeepLeaf_ReturnsTrue()
    {
        Assert.True(
            _organization.HasPermission("db.admin"));
    }

    [Fact]
    public void HasPermission_NobodyHasIt_ReturnsFalse()
    {
        Assert.False(
            _organization.HasPermission("superadmin"));
    }

    [Theory]
    [InlineData("api.read")]
    [InlineData("api.write")]
    [InlineData("db.admin")]
    [InlineData("ui.deploy")]
    [InlineData("billing.read")]
    [InlineData("billing.write")]
    public void HasPermission_AllKnownPermissions(
        string permission)
    {
        Assert.True(
            _organization.HasPermission(permission));
    }

    [Fact]
    public void GetAllPermissions_ReturnsDeduplicatedSet()
    {
        var permissions =
            _organization.GetAllPermissions();

        Assert.Equal(6, permissions.Count);
        Assert.Contains("api.read", permissions);
        Assert.Contains("api.write", permissions);
        Assert.Contains("db.admin", permissions);
        Assert.Contains("ui.deploy", permissions);
        Assert.Contains("billing.read", permissions);
        Assert.Contains("billing.write", permissions);
    }

    [Fact]
    public void GetAllPermissions_DuplicatePermission_CountedOnce()
    {
        var permissions =
            _organization.GetAllPermissions();

        var apiReadCount = permissions
            .Count(p => p.Equals(
                "api.read",
                System.StringComparison.OrdinalIgnoreCase));

        Assert.Equal(1, apiReadCount);
    }

    [Fact]
    public void HasPermission_DepartmentLevel_FindsUserPermission()
    {
        var department = new Department("Engineering");
        var team = new Team("Backend");
        team.Add(new User(
            "Alice",
            new[] { "api.read" }));
        department.Add(team);

        Assert.True(
            department.HasPermission("api.read"));
    }

    [Fact]
    public void Remove_Child_PermissionNoLongerFound()
    {
        var team = new Team("Backend");
        var alice = new User(
            "Alice",
            new[] { "api.read" });
        team.Add(alice);

        Assert.True(team.HasPermission("api.read"));

        team.Remove(alice);

        Assert.False(team.HasPermission("api.read"));
    }
}

The test structure follows a deliberate pattern. UserTests verify the leaf node in isolation -- direct permission lookups, case insensitivity, grant, and revoke. TeamTests verify single-level composition and nested sub-teams. OrganizationHierarchyTests build the full tree from the recursive permission resolution section and verify end-to-end behavior. The [Theory] test with [InlineData] is particularly useful because it confirms that every known permission in the hierarchy is discoverable from the root, exercising every path through the tree with a single test method.

The Remove_Child_PermissionNoLongerFound test validates an important production concern: when you remove a component from the tree, its permissions disappear from all queries. This behavior is automatic with the composite pattern because HasPermission and GetAllPermissions always traverse the current children. There's no stale cache to invalidate -- the tree structure is the source of truth. You'll find this same testability advantage in other structural patterns. The strategy pattern benefits from the same interface-driven test doubles, and the observer pattern uses isolated subscriber tests that parallel the leaf-level tests shown here.

Frequently Asked Questions

How do I prevent circular references in a composite tree?

Circular references happen when a composite node is added as a child of one of its own descendants, creating an infinite loop during traversal. The simplest prevention is to check for cycles in the Add method by walking up the tree or traversing the subtree being added to confirm that the parent doesn't appear in it. For large trees where traversal cost matters, maintain a flat lookup of ancestor chains and validate insertions against it. In practice, most organizational hierarchies are built from configuration or database records where cycles are structurally impossible, so the overhead of runtime cycle detection is rarely justified.

Can I combine the composite pattern with other design patterns?

Absolutely -- and it's common in production systems. The decorator pattern wraps a composite node to add cross-cutting concerns like logging or caching without modifying the node itself. The visitor pattern pairs with composite to add new operations over the tree without changing the node classes. The iterator pattern formalizes the traversal strategy (depth-first vs. breadth-first). You can find an overview of how these patterns complement each other in the big list of design patterns.

Is the composite pattern the right choice when my hierarchy has only two levels?

If your hierarchy is genuinely fixed at two levels -- say, teams and users -- you can solve the problem with a simple container class that holds a list of users. The composite pattern adds value when the hierarchy depth is variable or when you want to treat individual items and groups uniformly through a shared interface. If there's any chance the hierarchy will grow deeper (adding departments, divisions, or regions above teams), building with the composite pattern from the start saves you from a painful refactor later.

How do I handle permissions that should be denied at a higher level?

The implementation in this article models additive permissions -- if any node in the subtree has a permission, the composite reports it as present. For deny rules, add a separate GetDeniedPermissions method to the interface and resolve conflicts with a priority system (deny overrides allow, or closest-to-leaf wins). An alternative approach is to use a PermissionResult enum with Allow, Deny, and Unset values instead of a boolean, letting each node contribute a vote that a resolution strategy combines.

What's the performance impact of deep recursive trees?

Each call to HasPermission or GetAllPermissions traverses the subtree rooted at the target node. For HasPermission, LINQ's Any short-circuits on the first match, so best-case performance is O(1) and worst-case is O(n) where n is the total number of leaf nodes. For GetAllPermissions, every leaf is always visited, making it O(n). In practice, organizational hierarchies rarely exceed a few hundred nodes, and the traversal is fast enough to run in a web request. For larger trees or high-throughput scenarios, cache the aggregated permission set at each composite node and invalidate the cache when children change.

How does this differ from a role-based access control (RBAC) system?

RBAC assigns permissions to roles, then assigns roles to users. The composite pattern models the organizational structure itself and resolves permissions by traversing it. The two approaches complement each other. You could use the composite pattern to model the organization and attach RBAC roles to each node. The composite's GetAllPermissions would then aggregate permissions from roles rather than raw permission strings, giving you hierarchical role inheritance on top of a standard RBAC system.

Should I use an abstract base class instead of an interface for the component?

An abstract base class lets you share the traversal logic (HasPermission, GetAllPermissions) across composites, eliminating the duplication you see between Team, Department, and Organization. The trade-off is that C# only supports single inheritance, so your composite nodes can't extend any other base class. If all composites share identical traversal logic and you don't need a different base class, an abstract CompositeBase is a reasonable choice. If your composites have meaningfully different traversal strategies or you want to keep the flexibility of interface-only contracts, stick with the interface.

Wrapping Up This Composite Pattern Real-World Example

This composite pattern real-world example in C# demonstrates the pattern solving a genuine business problem -- turning a brittle, type-checking permission system into a clean hierarchy where every node answers the same questions regardless of whether it's a single user or an entire organization. We started with a PermissionChecker class that needed seven parameters and a chain of if/else blocks to traverse the hierarchy. We ended with four classes that each implement IPermissionComponent and delegate queries recursively through the tree.

The composite pattern isn't about adding layers of abstraction for the sake of architecture. It's about making a tree structure queryable through a single interface. Permission checks, name lookups, and permission aggregation all work identically at every level of the hierarchy. When a new level appears -- a division, a region, a subsidiary -- you write one class that implements IPermissionComponent, add it to the tree, and everything continues to work.

Take this composite pattern real-world example in C#, replace the permission domain with your own hierarchy -- file systems, menu structures, organizational charts, pricing tiers -- and start building your own composite tree. The pattern works the same way every time: define the shared interface, implement it in leaves and composites, and let the recursion handle the rest.

Composite Pattern In C# For Powerful Object Structures

Learn how to implement the Composite Pattern in C#! This article explores the pros and the cons of this design pattern and how it works in C#. Check it out!

Composite Design Pattern in C#: Complete Guide with Examples

Master the composite design pattern in C# with practical examples showing tree structures, recursive operations, and uniform component handling.

When to Use Composite Pattern in C#: Decision Guide with Examples

Discover when to use composite pattern in C# with real decision criteria, use case examples, and guidance on when simpler alternatives work better.

An error has occurred. This application may no longer respond until reloaded. Reload