👋Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.
Structural Design Patterns in .NET: All 7 Patterns with Full Working Code
GoF structural patterns in .NET: Adapter, Decorator, Facade, Proxy. With C# examples.
October 24, 2025 · Waqas Ahmad
Read the article
Introduction
This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).
Structural design patterns address how classes and objects are composed—wrapping, simplifying, and sharing—but are misused when applied without a real composition problem (e.g. Bridge with one implementation, Composite without a tree). This article covers all seven GoF structural patterns in .NET: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy—each with definition, when to use it, class diagram, and full C# examples. For architects and tech leads, picking the right pattern for the problem (Adapter for integration, Decorator for behaviour, Facade for subsystems, Proxy for lazy/access) keeps code clear; over-application adds indirection without benefit.
Structural patterns describe recurring ways to compose types and objects: wrapping one interface to match another, separating abstraction from implementation, treating trees uniformly, adding responsibilities dynamically, simplifying subsystems, sharing state to save memory, and providing placeholders or access control. There are seven structural patterns; the table below lists all of them.
Pattern
Problem it solves
Typical .NET use
Adapter
Client expects interface A; you have interface B
Legacy/third-party wrappers, SDK adapters
Bridge
Abstraction and implementation should vary independently
Pluggable drivers, UI + platform
Composite
Treat tree of objects uniformly (part–whole)
UI trees, file systems, expressions
Decorator
Add behavior at runtime without subclassing
Logging, caching, retry wrappers
Facade
Simple interface to a complex subsystem
Service layers, workflow entry points
Flyweight
Many similar objects; share state to save memory
Character/icon caches, shared config
Proxy
Placeholder or control access to another object
Lazy load, access control, remoting
Decision Context
System scale: Varies by context; the approach in this article applies to the scales and scenarios described in the body.
Team size: Typically small to medium teams; ownership and clarity matter more than headcount.
Time / budget pressure: Applicable under delivery pressure; I’ve used it in both greenfield and incremental refactors.
Technical constraints: .NET and related stack where relevant; constraints are noted in the article where they affect the approach.
Non-goals: This article does not optimize for every possible scenario; boundaries are stated where they matter.
All structural patterns at a glance
Adapter: Wrap an existing interface so it matches the interface the client expects. Use for legacy APIs, external SDKs, and different data shapes.
Bridge: Separate abstraction from implementation so both can vary independently. Use when you have multiple dimensions (e.g. abstraction types × implementations).
Composite: Compose objects into tree structures; treat individual objects and compositions uniformly. Use for UI hierarchies, file systems, and expression trees.
Decorator: Add responsibilities to an object dynamically by wrapping it. Use for logging, caching, retries, and cross-cutting concerns.
Facade: Provide a simple interface to a complex subsystem. Use for order placement, onboarding flows, and multi-step workflows.
Flyweight: Share state between many similar objects to save memory. Use for character glyphs, repeated icons, and shared configuration.
Proxy: Provide a surrogate or placeholder for another object. Use for lazy loading, access control, caching, and remoting.
Adapter pattern
What it is and when to use it
Adapter wraps an existing interface so it matches the interface the client expects. Use it when you have legacy or third-party code that does not match your domain interface—the adapter translates calls and data so the client stays unchanged. Typical uses: legacy APIs, external SDKs, DTO → domain mapping, and wrapping non-async code in async interfaces.
Class structure
Loading diagram…
Class structure explained: The client depends on ITarget (the interface you want). The Adaptee is the existing type with a different interface (SpecificRequest). The Adapter implements ITarget, holds the Adaptee, and in Request() calls _adaptee.SpecificRequest() (and may map data). The client receives an ITarget and never sees the Adaptee.
How this code fits together: The client depends only on IOrderService. You register LegacyOrderAdapter as the implementation; it holds LegacyOrderApi and translates GetByIdAsync(id) into FetchOrder(id) and maps the DTO to Order. The client never sees the legacy API.
When to use Adapter: Use when you must integrate code that has a different interface (legacy, third-party, another team’s SDK) and you want the rest of your app to depend on your own interface. Avoid when you control both sides—prefer a single, consistent interface.
Bridge pattern
What it is and when to use it
Bridge separates abstraction from implementation so both can vary independently. Use it when you have multiple abstraction types and multiple implementations and you want to avoid a Cartesian explosion of subclasses (e.g. RedCircle, BlueCircle, RedSquare…). The abstraction holds a reference to the implementation interface; callers work with the abstraction, and the implementation can be swapped.
Class structure
Loading diagram…
Class structure explained:Abstraction (or a refined subclass) holds an IImplementor and delegates implementation details to it. ConcreteImplementorA/B provide different implementations. The client uses the abstraction; you inject the implementor (e.g. via DI or config), so you can change implementation without changing abstraction hierarchy.
Full working example: Renderer abstraction and platform implementations
1. Implementor interface and concrete implementors
var htmlReport = new ReportDocument(new HtmlRenderer());
var plainReport = new ReportDocument(new PlainTextRenderer());
Console.WriteLine(htmlReport.GetContent()); // <p>Report: data</p>
Console.WriteLine(plainReport.GetContent()); // Report: data
How this code fits together:Document (abstraction) holds an IRenderer (implementor). ReportDocument calls Renderer.Render(...) inside GetContent(). You compose at runtime: same ReportDocument with different renderers produces different output. Adding a new document type or a new renderer does not require a new class for every combination.
When to use Bridge: Use when you have two independent dimensions (e.g. document types × output formats, UI controls × platforms) and want to avoid a multiplication of subclasses. Avoid when you have only one implementation or one abstraction—simplify instead.
Composite pattern
What it is and when to use it
Composite composes objects into tree structures so clients can treat individual objects and compositions uniformly. Use it for UI hierarchies (controls containing controls), file systems (files and folders), expression trees, and any part–whole structure where you want a single interface for leaves and containers.
Class structure
Loading diagram…
Class structure explained:IComponent defines the common interface (e.g. Operation(), and optionally Add/Remove/GetChild). Leaf implements Operation() and does nothing for add/remove (or throws). Composite holds a list of IComponent children, implements Operation() by delegating to each child, and implements Add/Remove/GetChild. The client treats IComponent uniformly; it can be a leaf or a composite.
Full working example: File system (files and folders)
1. Component interface
namespaceCompositeExample;
publicinterfaceIFileSystemNode {
string Name { get; }
intGetSize();
}
2. Leaf (file)
namespaceCompositeExample;
publicclassFileNode : IFileSystemNode {
publicstring Name { get; }
privatereadonlyint _size;
publicFileNode(string name, int size) { Name = name; _size = size; }
publicintGetSize() => _size;
}
var root = new FolderNode("root");
root.Add(new FileNode("a.txt", 100));
var sub = new FolderNode("sub");
sub.Add(new FileNode("b.txt", 200));
root.Add(sub);
Console.WriteLine(root.GetSize()); // 300
How this code fits together: The client works with IFileSystemNode. A FileNode returns its size; a FolderNode sums the sizes of its children. You build the tree by adding nodes to folders. The same GetSize() call works on any node—uniform treatment of leaf and composite.
When to use Composite: Use when you have a part–whole hierarchy and want to treat leaves and containers the same way (single interface, recursive operations). Avoid when the tree is shallow or when leaves and composites have very different operations—consider separate types then.
Decorator pattern
What it is and when to use it
Decorator adds responsibilities to an object dynamically without subclassing. You wrap the object in a decorator that implements the same interface, delegates to the inner object, and adds behavior before/after. Use it for logging, caching, retries, validation, and cross-cutting concerns; compose at runtime via DI.
Class structure
Loading diagram…
Class structure explained:IComponent is the common interface. ConcreteComponent is the core implementation. Decorator implements IComponent, holds an IComponent (_inner), and in Operation() typically calls _inner.Operation() and adds behavior (e.g. log before/after). ConcreteDecoratorA is a concrete decorator (e.g. logging). You can stack decorators (e.g. Logging(Caching(Core))).
Full working example: Logging decorator for order service
IOrderService svc = new LoggingOrderService(new OrderService(), Console.WriteLine);
var order = await svc.GetByIdAsync("O1");
How this code fits together: The client depends on IOrderService. You wrap the core OrderService in LoggingOrderService; the decorator delegates to _inner.GetByIdAsync and logs. In DI you register the decorator wrapping the core so the same interface is used everywhere.
When to use Decorator: Use when you need to add behavior at runtime (logging, caching, retry, validation) without changing the core type and without a fixed inheritance tree. Avoid when you have only one fixed wrapper—a simple wrapper class may suffice.
Facade pattern
What it is and when to use it
Facade provides a simple interface to a complex subsystem. One entry point hides multiple dependencies and the order of operations. Use it for order placement (inventory + payment + shipping), onboarding flows, report generation, and any multi-step workflow where callers need one high-level operation.
Class structure
Loading diagram…
Class structure explained: The Facade holds references to subsystem types (A, B, C) and exposes one or a few high-level methods (e.g. PlaceOrder). Inside, it calls _a.DoA(), _b.DoB(), _c.DoC() in the right order and handles errors. Callers depend only on the Facade, not on the subsystems.
How this code fits together: The client calls PlaceOrderAsync once. The Facade coordinates inventory, payment, and shipping in sequence. The client does not know about the three subsystems or the order of calls.
When to use Facade: Use when you want one simple entry point to a set of related types or steps. Avoid when callers need fine-grained control over each step—expose the subsystems or use a different abstraction.
Flyweight pattern
What it is and when to use it
Flyweight uses sharing to support large numbers of fine-grained objects efficiently. Intrinsic state (shared, immutable) is stored in the flyweight; extrinsic state (varying per use) is passed in by the client. Use it for character glyphs, repeated icons, shared configuration, or any time many objects share the same underlying data.
Class structure
Loading diagram…
Class structure explained:Flyweight holds intrinsic state (shared, immutable). FlyweightFactory returns a flyweight for a given key (e.g. character code), creating and caching it. The client passes extrinsic state (e.g. position, color) into Operation each time. Many logical “objects” share few flyweight instances.
Full working example: Character glyph cache
1. Flyweight and factory
namespaceFlyweightExample;
publicclassCharGlyph {
privatereadonlychar _char;
publicCharGlyph(char c) => _char = c;
publicvoidDraw(int x, int y) { /* draw _char at (x,y) */ }
}
publicclassGlyphFactory {
privatereadonly Dictionary<char, CharGlyph> _cache = new();
public CharGlyph GetGlyph(char c) {
if (!_cache.TryGetValue(c, outvar g)) { g = new CharGlyph(c); _cache[c] = g; }
return g;
}
}
2. Usage
var factory = new GlyphFactory();
factory.GetGlyph('A').Draw(0, 0);
factory.GetGlyph('A').Draw(10, 0); // same instance reused
How this code fits together:GlyphFactory ensures one CharGlyph per character. The client calls GetGlyph(c) and then Draw(x, y) with position (extrinsic). Many uses of ‘A’ share one CharGlyph instance.
When to use Flyweight: Use when you have many objects that share large amounts of identical state and you want to reduce memory. Avoid when objects do not share state or when the factory lookup cost outweighs savings.
Proxy pattern
What it is and when to use it
Proxy provides a surrogate or placeholder for another object. Use it for lazy loading (create real object on first use), access control (check permissions before delegating), caching (return cached result), remoting (local stub for remote object), or logging (log then delegate). The client uses the same interface as the real subject.
Class structure
Loading diagram…
Class structure explained:ISubject is the interface. RealSubject does the real work. Proxy implements ISubject, holds a reference to RealSubject (or a factory), and in Request() may create the real object (lazy), check access, cache, or log, then delegate to the real subject.
var proxy = new LazyReportProxy(() => new HeavyReport());
// HeavyReport not created yetbyte[] data = proxy.Generate(); // creates HeavyReport, then generates
How this code fits together: The client holds an IReport. LazyReportProxy defers creating HeavyReport until Generate() is first called; subsequent calls reuse the same instance. The client does not know whether it has the real object or the proxy.
When to use Proxy: Use when you need lazy initialization, access control, caching, or remoting while keeping the same interface as the real object. Avoid when you do not need indirection—use the real type directly.
Comparison: when to use which
Pattern
Use when
Avoid when
Adapter
Legacy/third-party has different interface
You control both sides
Bridge
Abstraction and implementation vary independently
Single implementation or abstraction
Composite
Part–whole tree; uniform treatment of leaf and composite
Shallow tree or very different operations
Decorator
Add behavior at runtime without subclassing
Single fixed wrapper
Facade
One simple entry point to a subsystem
Callers need fine-grained control
Flyweight
Many objects share large identical state
No shared state or small object count
Proxy
Lazy load, access control, cache, remoting
No need for indirection
Common pitfalls
Adapter: Making the adapter too thick (business logic). Keep it as a thin translation layer.
Bridge: Confusing with Adapter. Bridge separates abstraction from implementation; Adapter changes an interface to match another.
Composite: Letting leaves implement add/remove and throw, or forgetting to treat leaf and composite uniformly.
Decorator: Forgetting to delegate to the inner object, or creating circular decorator chains.
Facade: Letting the facade become a god object; keep it a thin coordinator.
Flyweight: Storing extrinsic state in the flyweight; pass it in per call.
Proxy: Proxying when a simple wrapper or lazy field would suffice.
Summary
Structural patterns in .NET—Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy—address how types and objects are composed; this article covered all seven with class diagrams and full C# examples. Using them without a real composition problem (e.g. Bridge with one implementation, Composite without a tree) adds confusion; applying them where the problem fits (Adapter for legacy integration, Decorator for cross-cutting behaviour, Facade for subsystems, Proxy for access or lazy loading) keeps design understandable. Next, identify one composition or interface problem in your codebase—integrating a legacy API, simplifying a subsystem, or adding behaviour without subclassing—then apply the matching pattern from this article and combine with DI as needed.
Position & Rationale
I apply structural patterns when they solve a concrete composition or interface problem—Adapter when I must integrate a type I can’t change, Decorator when I need to add behaviour without subclassing, Facade when I want a single entry point to a subsystem, Proxy when I need lazy loading or access control. I avoid using a pattern when a simple wrapper, interface, or helper would do; I don’t add Bridge or Composite for hypothetical future variation. I prefer Decorator over deep inheritance for cross-cutting behaviour (e.g. logging, caching). I use Adapter to wrap legacy or third-party APIs so the rest of the app depends on our interface. I keep Facade thin so it doesn’t become a god object. I reject applying Flyweight or Proxy when there’s no real sharing or access-control need—they add indirection without payoff.
Trade-Offs & Failure Modes
What this sacrifices: Each pattern adds types and indirection; over-use makes the codebase harder to follow. Adapter and Facade can hide design debt instead of fixing it.
Where it degrades: When we use a pattern “because the book says so” without a clear problem—e.g. Bridge when we have one implementation, Composite when we never have a tree. Decorator chains can get deep and hard to debug.
How it fails when misapplied: Adapter wrapping something we could refactor; Facade becoming a dump of all subsystem calls; Flyweight when there’s no meaningful shared state; Proxy when a lazy field or simple check would suffice.
Early warning signs: “We added three layers and now we don’t know where the bug is”; “the facade has 50 methods”; “we use Proxy everywhere.”
What Most Guides Miss
Guides often show one pattern in isolation and don’t help you choose. In practice, Adapter vs Facade vs Decorator depends on whether you’re adapting an external type, simplifying a subsystem, or adding behaviour. Composite is easy to over-engineer when you don’t actually have a recursive structure. Bridge is rarely needed with one implementation—wait for a real abstraction/implementation split. Flyweight only pays off when you have many objects sharing significant intrinsic state; otherwise it’s ceremony. The “when not to use” and the cost of indirection are underplayed.
Decision Framework
If you need to use a type you can’t change → Adapter; expose the interface your app needs.
If you need to add behaviour without subclassing → Decorator; keep the chain shallow and each decorator focused.
If you need a single entry point to a complex subsystem → Facade; keep it thin, no business logic.
If you have a recursive part-whole structure → Composite; otherwise avoid.
If you need lazy loading or access control → Proxy; otherwise a lazy field or guard may be enough.
If you have many objects sharing heavy intrinsic state → Flyweight; otherwise skip.
If abstraction and implementation vary independently → Bridge; if only one implementation, wait.
You can also explore more patterns in the .NET Architecture resource page.
Key Takeaways
Use structural patterns for real composition problems: Adapter for integration, Decorator for behaviour, Facade for subsystems, Proxy for lazy/access control.
Avoid over-application: no Bridge with one implementation, no Composite without a tree, no Flyweight without shared state.
Keep Facade thin; keep Decorator chains focused. Revisit when the problem shape changes.
Need architectural guidance for real-world .NET platforms? I offer consulting for .NET architecture, API platforms, and enterprise system design.
When I Would Use This Again — and When I Wouldn’t
I would use structural patterns again when I have a clear composition or interface problem—integrating a legacy API (Adapter), adding cross-cutting behaviour without subclassing (Decorator), simplifying a subsystem (Facade), or controlling access or lazy loading (Proxy). I wouldn’t use Bridge until I have at least two implementation families; I wouldn’t use Composite without a real tree structure; I wouldn’t use Flyweight without many objects sharing significant state. For simple wrappers or one-off helpers, I’d skip the pattern. If the team finds the indirection confusing, I’d simplify to the minimal structure that solves the problem.
Frequently Asked Questions
Frequently Asked Questions
What is Adapter?
Adapter wraps an existing interface so it matches the interface the client expects. Use when integrating legacy or third-party code.
What is Bridge?
Bridge separates abstraction from implementation so both can vary independently. Use when you have multiple abstraction types and multiple implementations.
What is Composite?
Composite composes objects into tree structures so you can treat individual objects and compositions uniformly. Use for UI trees, file systems, expression trees.
What is Decorator?
Decorator adds responsibilities to an object dynamically without subclassing. Use for logging, caching, retries, validation.
What is Facade?
Facade provides a simple interface to a complex subsystem. Use for order placement, onboarding, multi-step workflows.
What is Flyweight?
Flyweight uses sharing to support many fine-grained objects efficiently. Intrinsic state is in the flyweight; extrinsic state is passed in. Use for character glyphs, repeated icons.
What is Proxy?
Proxy provides a surrogate for another object. Use for lazy loading, access control, caching, remoting.
When use Adapter vs Facade?
Adapter converts one interface to another. Facade simplifies a subsystem with one entry point. Both wrap; Adapter changes interface, Facade hides complexity.
When use Decorator vs inheritance?
Decorator adds behavior at runtime by wrapping. Inheritance adds behavior at compile time. Use Decorator when you need to compose multiple behaviors (e.g. logging + caching).
Adapter vs Decorator?
Adapter changes the interface of the wrapped object. Decorator implements the same interface and adds behavior. Both wrap; different goals.
How implement Decorator in .NET?
Create a class that implements the same interface as the inner component, inject the inner via constructor, delegate calls to it, and add your behavior (e.g. log before/after). Register in DI: decorator wrapping core.
Proxy: virtual vs protection?
Virtual proxy creates the real object on first use (lazy load). Protection proxy checks permissions before delegating. Same pattern, different intent.
Flyweight: intrinsic vs extrinsic?
Intrinsic state is shared and stored in the flyweight (e.g. character code). Extrinsic state varies per use and is passed in by the client (e.g. position, color).
Related Guides & Resources
Explore the matching guide, related services, and more articles.