This is Part 2 of a three-part design patterns series built for interview prep. Part 1 covered Creational patterns - how objects get created. This part covers Structural patterns - how objects and classes are composed into larger structures. Same format throughout: the problem each pattern solves, an analogy, a before-and-after code example, and honest guidance on when it's actually worth the added complexity.
Structural patterns include Adapter, Decorator, Facade, Composite, Proxy, and Bridge. This post covers the four that come up most often in real interviews and real code: Adapter, Decorator, Facade, and Composite.
Pattern 1: Adapter
The problem Adapter solves is making two interfaces that were never designed to work together actually work together, without modifying either one.
Think of a physical power plug adapter. A US laptop charger and a European wall socket are both perfectly functional on their own, but their shapes are incompatible. Nobody rewires the wall or rebuilds the charger - a small adapter sits between them, translating one shape into the other. Adapter does the same thing in code: it translates one interface into another without changing either side.
// The interface your code already expects
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
// A third-party library with an incompatible interface
// you cannot modify - this is someone else's code
public class LegacyPaymentGateway
{
public void MakeTransaction(double amountInCents)
{
Console.WriteLine($"Processing {amountInCents} cents");
}
}
// WITHOUT Adapter - the rest of the codebase has to
// know about LegacyPaymentGateway's specific method
// signature and unit conventions directly
public class CheckoutService
{
private readonly LegacyPaymentGateway _gateway = new();
public void Checkout(decimal amount)
{
// every caller needs to remember to convert
// dollars to cents, and call MakeTransaction
// specifically - tightly coupled to this one
// library's exact shape
_gateway.MakeTransaction((double)(amount * 100));
}
}
// WITH Adapter - the incompatibility is isolated
// to one small class
public class LegacyPaymentAdapter : IPaymentProcessor
{
private readonly LegacyPaymentGateway _gateway;
public LegacyPaymentAdapter(LegacyPaymentGateway gateway)
=> _gateway = gateway;
public void ProcessPayment(decimal amount)
{
// the unit conversion and method name
// mismatch lives HERE, in exactly one place
double amountInCents = (double)(amount * 100);
_gateway.MakeTransaction(amountInCents);
}
}
// The rest of the codebase depends only on
// IPaymentProcessor - it never knows LegacyPaymentGateway
// exists at all
public class CheckoutService
{
private readonly IPaymentProcessor _processor;
public CheckoutService(IPaymentProcessor processor)
=> _processor = processor;
public void Checkout(decimal amount)
=> _processor.ProcessPayment(amount);
}
var adapter = new LegacyPaymentAdapter(new LegacyPaymentGateway());
var checkout = new CheckoutService(adapter);
checkout.Checkout(49.99m);
Adapter earns its place specifically around third-party libraries, legacy code, or any external dependency whose interface doesn't match what the rest of the codebase expects and can't be changed directly. It's not worth introducing between two pieces of code that are both already under direct control - in that case, simply changing one interface to match the other is the more direct fix, with no adapter layer needed at all.
Pattern 2: Decorator
The problem Decorator solves is adding behavior to a single object instance dynamically, without modifying its class, without affecting other instances of that same class, and without relying on subclassing.
Think of a plain coffee, with milk added, then a shot of caramel added on top of that. Each addition wraps the drink before it, adding its own contribution, without needing a separate class for every possible combination - CoffeeWithMilk, CoffeeWithCaramel, CoffeeWithMilkAndCaramel, and so on infinitely. Decorator wraps objects the same way, layer by layer, at runtime.
// The base interface
public interface ICoffee
{
decimal Cost();
string Description();
}
public class PlainCoffee : ICoffee
{
public decimal Cost() => 2.00m;
public string Description() => "Coffee";
}
// WITHOUT Decorator - a new subclass for every
// possible combination of add-ons
public class CoffeeWithMilk : PlainCoffee { /* ... */ }
public class CoffeeWithCaramel : PlainCoffee { /* ... */ }
public class CoffeeWithMilkAndCaramel : PlainCoffee { /* ... */ }
// Adding a third add-on option means the number of
// subclasses needed grows combinatorially
// WITH Decorator - each add-on is a small wrapper
// class that can be combined at runtime, in any order
public abstract class CoffeeDecorator : ICoffee
{
protected readonly ICoffee _coffee;
protected CoffeeDecorator(ICoffee coffee)
=> _coffee = coffee;
public virtual decimal Cost() => _coffee.Cost();
public virtual string Description()
=> _coffee.Description();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override decimal Cost() => _coffee.Cost() + 0.50m;
public override string Description()
=> _coffee.Description() + " + milk";
}
public class CaramelDecorator : CoffeeDecorator
{
public CaramelDecorator(ICoffee coffee) : base(coffee) { }
public override decimal Cost() => _coffee.Cost() + 0.75m;
public override string Description()
=> _coffee.Description() + " + caramel";
}
// Combine any way, at runtime, without a single
// new subclass for the combination
ICoffee order = new CaramelDecorator(
new MilkDecorator(new PlainCoffee())
);
Console.WriteLine(order.Description()); // Coffee + milk + caramel
Console.WriteLine(order.Cost()); // 3.25
This pattern is not just a textbook exercise - it's how the .NET Stream classes actually work. new GZipStream(new FileStream(path, FileMode.Open), CompressionMode.Compress) wraps a FileStream with compression behavior, and more decorators like encryption or buffering can wrap that further, each layer adding one capability without changing any of the classes underneath.
Decorator earns its place when behavior needs to be added or removed from individual object instances at runtime, in combinations that would otherwise require an unmanageable number of subclasses. For a fixed, small set of variations known entirely at compile time, straightforward inheritance or simple composition without the full decorator pattern is often simpler and easier to follow.
Pattern 3: Facade
The problem Facade solves is providing one simple, unified interface to a subsystem made up of many complex, interacting parts, so calling code doesn't need to understand or coordinate all of those parts directly.
Think of a car's ignition. Turning the key, or pressing the start button, triggers fuel injection, spark timing, starter motor engagement, and a dozen other coordinated subsystems - the driver never manages any of that directly. One simple action hides a genuinely complicated system underneath. Facade provides that same single, simple entry point in code.
// A genuinely complex subsystem with several
// interacting parts - this complexity is real
// and does not go away, it just gets hidden
public class InventorySystem
{
public bool CheckStock(string sku, int qty)
=> true; // simplified
}
public class PaymentSystem
{
public bool ChargeCard(string cardToken, decimal amount)
=> true; // simplified
}
public class ShippingSystem
{
public string ScheduleDelivery(string address)
=> "TRACK123"; // simplified
}
public class NotificationSystem
{
public void SendConfirmation(string email, string trackingNumber)
=> Console.WriteLine($"Confirmation sent: {trackingNumber}");
}
// WITHOUT Facade - calling code has to know about
// and correctly coordinate all four systems itself
public void PlaceOrderWithoutFacade(
string sku, int qty, string cardToken,
decimal amount, string address, string email)
{
var inventory = new InventorySystem();
var payment = new PaymentSystem();
var shipping = new ShippingSystem();
var notification = new NotificationSystem();
if (inventory.CheckStock(sku, qty))
{
if (payment.ChargeCard(cardToken, amount))
{
var tracking = shipping.ScheduleDelivery(address);
notification.SendConfirmation(email, tracking);
}
}
// this coordination logic now has to be repeated
// correctly everywhere an order gets placed
}
// WITH Facade - one simple entry point
public class OrderFacade
{
private readonly InventorySystem _inventory = new();
private readonly PaymentSystem _payment = new();
private readonly ShippingSystem _shipping = new();
private readonly NotificationSystem _notification = new();
public bool PlaceOrder(
string sku, int qty, string cardToken,
decimal amount, string address, string email)
{
if (!_inventory.CheckStock(sku, qty))
return false;
if (!_payment.ChargeCard(cardToken, amount))
return false;
var tracking = _shipping.ScheduleDelivery(address);
_notification.SendConfirmation(email, tracking);
return true;
}
}
// Calling code is now trivially simple, and the
// coordination logic exists in exactly one place
var facade = new OrderFacade();
facade.PlaceOrder("SKU-001", 1, "tok_abc", 49.99m,
"123 Main St", "customer@example.com");
Facade earns its place whenever calling code repeatedly needs to coordinate several subsystems in the same sequence - centralizing that coordination in one place prevents the same multi-step logic from being duplicated, and inevitably drifting out of sync, across multiple call sites. It's not about making the underlying subsystem less complex - that complexity is still there - it's about giving callers a simple, correct way to use it without needing to understand all of it directly.
Pattern 4: Composite
The problem Composite solves is treating a single object and a group of objects through the exact same interface, so calling code doesn't need separate logic for "one item" versus "a collection of items."
Think of a file system. A folder can contain files, and it can also contain other folders, which themselves contain files or more folders, arbitrarily deep. When calculating how much space a folder uses, the calculation works identically whether a given item turns out to be a single file or an entire nested folder - each one just needs to be able to answer "how big are you." Composite is exactly this: treating individual objects and groups of objects through one shared interface.
// The shared interface - both leaves and
// composites implement this
public interface IFileSystemItem
{
string Name { get; }
long GetSize();
}
// Leaf - a single file, no children
public class FileItem : IFileSystemItem
{
public string Name { get; }
private readonly long _size;
public FileItem(string name, long size)
{
Name = name;
_size = size;
}
public long GetSize() => _size;
}
// Composite - a folder, which can contain
// other IFileSystemItems (files OR folders)
public class FolderItem : IFileSystemItem
{
public string Name { get; }
private readonly List<IFileSystemItem> _children = new();
public FolderItem(string name) => Name = name;
public void Add(IFileSystemItem item)
=> _children.Add(item);
// The size of a folder is just the sum of
// whatever its children report - it does not
// matter whether each child is a file or
// another folder, the interface is identical
public long GetSize()
=> _children.Sum(child => child.GetSize());
}
// Building a nested structure
var root = new FolderItem("root");
var docs = new FolderItem("documents");
docs.Add(new FileItem("resume.pdf", 500));
docs.Add(new FileItem("cover-letter.pdf", 200));
var photos = new FolderItem("photos");
photos.Add(new FileItem("vacation.jpg", 3000));
root.Add(docs);
root.Add(photos);
root.Add(new FileItem("notes.txt", 50));
// One call, correctly recurses through the ENTIRE
// nested structure - no special-casing files vs
// folders anywhere in this calling code
Console.WriteLine(root.GetSize()); // 3750
Composite earns its place specifically when a domain is genuinely hierarchical - things that contain other things of the same conceptual type, arbitrarily deep - like file systems, UI component trees, or organizational charts. For a flat, non-nested collection of items with no "contains other items of the same type" relationship, Composite adds a layer of abstraction that isn't solving any real problem, and a plain list is simpler and more honest about the actual shape of the data.
Key Lessons
Adapter isolates an incompatibility to one small class, and earns its place specifically around external dependencies that can't be modified directly - not between two pieces of code already under direct control.
Decorator adds behavior to individual object instances at runtime, avoiding a combinatorial explosion of subclasses - .NET's own Stream classes are a real, built-in example of this pattern in daily use.
Facade centralizes coordination logic for a complex subsystem into one simple entry point, without making the underlying subsystem itself any less complex.
Composite lets calling code treat a single item and a nested group of items identically, and earns its place specifically for genuinely hierarchical domains, not for flat collections with no real containment relationship.
Every structural pattern covered here is solving a coupling or duplication problem that becomes real friction at some scale - none of them should be reached for preemptively before that friction actually shows up.
What's Next
Part 3 covers Behavioral patterns - Strategy, Observer, Command, State, and Template Method - the patterns concerned with how objects communicate and share responsibility with each other. This closes out the series.
Summary
Structural patterns solve one consistent underlying question - how should these pieces actually fit together - in four different ways depending on the specific shape of the problem. Adapter reconciles two incompatible interfaces. Decorator adds behavior to individual instances without subclass explosion. Facade simplifies the calling code's relationship to a complex subsystem. Composite treats individual items and nested groups through one shared interface. As with the creational patterns covered in Part 1, each of these adds real indirection that only pays for itself once the specific problem it solves is genuinely present in the code.
Originally published at TechStack Blog: https://www.techstackblog.com/post.html?slug=design-patterns-structural-csharp
Part 1 of this series (Creational patterns): https://www.techstackblog.com/post.html?slug=design-patterns-creational-csharp
More from TechStack Blog: CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
Top comments (0)