./articles/ Software Engineering Essentials: Structural Design Patterns
Structural design patterns organize objects and classes into larger structures while keeping those structures flexible. They focus on composition boundaries: adapting interfaces, separating variation axes, treating parts and wholes uniformly, wrapping behavior, simplifying subsystem access, sharing state, and controlling access.
This chapter covers all seven Structural patterns in the Gang of Four catalog. Each section moves from a concrete composition problem to a creation or delegation flow, then provides independent Python and Java examples with matching demo output. For object creation and object collaboration, see the Creational and Behavioral chapters.
Project source: OOP Design Patterns Handbook: Structural Patterns.
Run each Python example independently with Python 3.10 or later; only the standard library is required. Each Java example is a separate source file named in its first comment. Compile and run its public class, for example javac -encoding UTF-8 AdapterDemo.java followed by java AdapterDemo. The Java examples target Java 8 or later and use only JDK APIs. Do not combine the snippets into one file.
Repository verification: the linked source could not be retrieved during preparation. These are independently tested Python reference examples, not verified reproductions of the repository's APIs or naming. No specific upstream discrepancy is asserted without inspecting that source.
Quick Reference
| Pattern | Core Intent | Key Benefit |
|---|---|---|
| Adapter | Convert an existing interface into one a client expects | Integrates incompatible code without changing either side |
| Bridge | Separate an abstraction from its implementation | Avoids a class for every combination of two variation axes |
| Composite | Treat individual objects and containers uniformly | Simplifies operations across part-whole hierarchies |
| Decorator | Attach responsibilities by wrapping an object | Combines optional behavior without subclass explosion |
| Facade | Offer one simplified entry point to subsystems | Coordinates a common workflow and reduces client coupling |
| Flyweight | Share immutable state across many objects | Reduces memory use when state repeats at scale |
| Proxy | Control access through a same-interface substitute | Adds protection, caching, logging, or deferred access |
The diagrams emphasize composition and delegation rather than exhaustive class relationships. On narrow screens, diagrams scroll horizontally to keep labels readable. Code and output remain visible if JavaScript is unavailable.
1. Adapter
Intent
Adapter converts one interface into another that a client expects. It lets existing code collaborate without changing either side.
The Problem It Solves
A checkout service works with integer cents and a charge() contract. A legacy gateway uses floating-point amounts and make_payment(). Rewriting the gateway may be impossible, while changing every checkout call would spread legacy details through the application. An adapter can translate between the two contracts at one boundary.
Structure
- Target:
PaymentGatewayis the interface the client already understands. - Adaptee:
LegacyGatewayhas the useful behavior but an incompatible method and value format. - Adapter:
LegacyGatewayAdapterimplements the target, converts cents to the legacy unit, and normalizes the response. - Client:
checkout()depends only on the target interface.
From Target Call to Legacy Response
- The client calls
charge(2499, "EUR")through the adapter. - The adapter validates the amount, converts it to 24.99, and calls the legacy gateway.
- It checks the legacy status and returns the payment reference as a string.
The demo uses an in-memory response rather than a network call. The adapter performs only format and protocol translation; it does not add payment authorization.
Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount_cents: int, currency: str) -> str:
pass
class LegacyGateway:
def make_payment(self, value: float, currency_code: str) -> dict[str, object]:
return {"status": "APPROVED", "reference": "LEG-421"}
class LegacyGatewayAdapter(PaymentGateway):
def __init__(self, gateway: LegacyGateway):
self._gateway = gateway
def charge(self, amount_cents: int, currency: str) -> str:
if amount_cents < 0:
raise ValueError("Amount must be non-negative")
result = self._gateway.make_payment(amount_cents / 100, currency)
if result["status"] != "APPROVED":
raise RuntimeError("Payment was not approved")
return str(result["reference"])
def checkout(gateway: PaymentGateway) -> str:
return f"Payment reference: {gateway.charge(2499, 'EUR')}"
if __name__ == "__main__":
print(checkout(LegacyGatewayAdapter(LegacyGateway())))Java Implementation
// AdapterDemo.java
interface PaymentGateway {
String charge(int amountCents, String currency);
}
final class LegacyGateway {
public java.util.Map<String, String> makePayment(double value, String currencyCode) {
return java.util.Map.of("status", "APPROVED", "reference", "LEG-421");
}
}
final class LegacyGatewayAdapter implements PaymentGateway {
private final LegacyGateway gateway;
public LegacyGatewayAdapter(LegacyGateway gateway) {
this.gateway = gateway;
}
public String charge(int amountCents, String currency) {
if (amountCents < 0) throw new IllegalArgumentException("Amount must be non-negative");
java.util.Map<String, String> result = gateway.makePayment(amountCents / 100.0, currency);
if (!"APPROVED".equals(result.get("status"))) {
throw new IllegalStateException("Payment was not approved");
}
return result.get("reference");
}
}
public class AdapterDemo {
static String checkout(PaymentGateway gateway) {
return "Payment reference: " + gateway.charge(2499, "EUR");
}
public static void main(String[] args) {
System.out.println(checkout(new LegacyGatewayAdapter(new LegacyGateway())));
}
}Output
Payment reference: LEG-421This is an object adapter: it holds the adaptee rather than inheriting from it. That keeps the legacy class replaceable and avoids exposing its public surface through inheritance. A class adapter, available in languages with multiple implementation inheritance, would inherit from both sides but couples the adapter more tightly to the adaptee.
Keep translation rules explicit. Here, cents become a decimal amount and an approved legacy status becomes a reference. Silently mapping every unexpected status or currency can hide an integration failure.
When to Use It
- When an existing class has the required behavior but not the interface your client can use.
- When integrating a third-party SDK or legacy service without modifying its source.
2. Bridge
Intent
Bridge splits one abstraction from one or more implementations so the two hierarchies can change independently. The abstraction holds a reference to an implementation interface rather than creating a concrete implementation itself.
The Problem It Solves
Shapes need vector and raster rendering. Inheritance alone produces a class for every combination: vector circle, raster circle, vector square, raster square, and so on. Bridge keeps shape variants in one hierarchy and renderer variants in another, then connects them through composition.
Structure
- Abstraction:
Shapeholds aRendererand definesdraw(). - Refined Abstractions:
CircleandSquaresupply their geometry and area calculation. - Implementation:
Rendererdeclaresrender(shape, area). - Concrete Implementations:
VectorRendererandRasterRendererprovide different output formats.
Combining Two Independent Variations
- Create a shape with the renderer selected at the composition boundary.
draw()calculates the shape-specific area and passes it to the renderer.- The renderer formats the result without knowing whether the shape is a circle or square.
The demo combines a vector circle and a raster square. Adding a triangle or an SVG renderer does not require a multiplication of shape-renderer subclasses.
Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
class Renderer(ABC):
@abstractmethod
def render(self, shape: str, area: float) -> str:
pass
class VectorRenderer(Renderer):
def render(self, shape: str, area: float) -> str:
return f"Vector {shape}: area={area:.2f}"
class RasterRenderer(Renderer):
def render(self, shape: str, area: float) -> str:
return f"Raster {shape}: area={area:.2f}"
class Shape(ABC):
def __init__(self, renderer: Renderer):
self._renderer = renderer
@abstractmethod
def area(self) -> float:
pass
def draw(self) -> str:
return self._renderer.render(type(self).__name__, self.area())
class Circle(Shape):
def __init__(self, renderer: Renderer, radius: float):
super().__init__(renderer)
self.radius = radius
def area(self) -> float:
return 3.141592653589793 * self.radius ** 2
class Square(Shape):
def __init__(self, renderer: Renderer, side: float):
super().__init__(renderer)
self.side = side
def area(self) -> float:
return self.side ** 2
if __name__ == "__main__":
shapes = [Circle(VectorRenderer(), 2), Square(RasterRenderer(), 3)]
for shape in shapes:
print(shape.draw())Java Implementation
// BridgeDemo.java
interface Renderer {
String render(String shape, double area);
}
final class VectorRenderer implements Renderer {
public String render(String shape, double area) {
return String.format(java.util.Locale.ROOT, "Vector %s: area=%.2f", shape, area);
}
}
final class RasterRenderer implements Renderer {
public String render(String shape, double area) {
return String.format(java.util.Locale.ROOT, "Raster %s: area=%.2f", shape, area);
}
}
abstract class Shape {
protected final Renderer renderer;
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract double area();
public String draw() {
return renderer.render(getClass().getSimpleName(), area());
}
}
final class Circle extends Shape {
private final double radius;
public Circle(Renderer renderer, double radius) {
super(renderer);
this.radius = radius;
}
public double area() {
return Math.PI * radius * radius;
}
}
final class Square extends Shape {
private final double side;
public Square(Renderer renderer, double side) {
super(renderer);
this.side = side;
}
public double area() {
return side * side;
}
}
public class BridgeDemo {
public static void main(String[] args) {
Shape[] shapes = {new Circle(new VectorRenderer(), 2), new Square(new RasterRenderer(), 3)};
for (Shape shape : shapes) {
System.out.println(shape.draw());
}
}
}Output
Vector Circle: area=12.57
Raster Square: area=9.00Bridge differs from Adapter in intent. Adapter reconciles incompatible interfaces that already exist; Bridge is designed in advance to keep two variation axes separate. In this example, a shape may hold any renderer, but it does not replace it after construction. That policy keeps the geometry and rendering configuration stable for the object's lifetime.
The shared draw() method uses the concrete class name only as a display label. A more complete design would pass an explicit shape descriptor rather than relying on the class name.
When to Use It
- When two independent dimensions of variation would otherwise create a large inheritance matrix.
- When implementation details, such as rendering technology or platform API, should change without altering domain abstractions.
3. Composite
Intent
Composite represents part-whole hierarchies through a common component interface. Clients can invoke the same operation on a leaf, a container, or an entire tree without branching on the object's structure.
The Problem It Solves
A course bundle contains a video product and a books bundle, which itself contains two products. Calculating the course price by manually traversing every level duplicates tree knowledge in every caller. A uniform total_price() operation lets each node contribute according to its role.
Structure
- Component:
CatalogItemdeclarestotal_price(). - Leaf:
Productreturns its own price. - Composite:
Bundlestores child components and sums their totals. - Client: works with
CatalogItemobjects and does not need to know whether an item is a product or bundle.
Recursive Price Calculation
- The client calls
total_price()on the outer course bundle. - The bundle asks each child for its total; the nested books bundle repeats the process.
- Leaf products return their own prices, and the sums propagate back to the caller.
The example uses non-negative integer cents to avoid floating-point money arithmetic. It does not model quantities, discounts, or duplicate-child detection.
Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
class CatalogItem(ABC):
@abstractmethod
def total_price(self) -> int:
pass
class Product(CatalogItem):
def __init__(self, name: str, price_cents: int):
if price_cents < 0:
raise ValueError("Price must be non-negative")
self.name = name
self.price_cents = price_cents
def total_price(self) -> int:
return self.price_cents
class Bundle(CatalogItem):
def __init__(self, name: str):
self.name = name
self._items: list[CatalogItem] = []
def add(self, item: CatalogItem) -> Bundle:
self._items.append(item)
return self
def total_price(self) -> int:
return sum(item.total_price() for item in self._items)
if __name__ == "__main__":
books = Bundle("Books").add(Product("Design Patterns", 4200)).add(Product("Refactoring", 3800))
course = Bundle("Course").add(Product("Video lessons", 9900)).add(books)
print(f"Books: {books.total_price()} cents")
print(f"Course: {course.total_price()} cents")Java Implementation
// CompositeDemo.java
import java.util.ArrayList;
import java.util.List;
interface CatalogItem {
int totalPrice();
}
final class Product implements CatalogItem {
private final String name;
private final int priceCents;
public Product(String name, int priceCents) {
if (priceCents < 0) throw new IllegalArgumentException("Price must be non-negative");
this.name = name;
this.priceCents = priceCents;
}
public int totalPrice() { return priceCents; }
}
final class Bundle implements CatalogItem {
private final String name;
private final List<CatalogItem> items = new ArrayList<>();
public Bundle(String name) { this.name = name; }
public Bundle add(CatalogItem item) {
items.add(item);
return this;
}
public int totalPrice() {
int total = 0;
for (CatalogItem item : items) total += item.totalPrice();
return total;
}
}
public class CompositeDemo {
public static void main(String[] args) {
Bundle books = new Bundle("Books")
.add(new Product("Design Patterns", 4200))
.add(new Product("Refactoring", 3800));
Bundle course = new Bundle("Course")
.add(new Product("Video lessons", 9900))
.add(books);
System.out.println("Books: " + books.totalPrice() + " cents");
System.out.println("Course: " + course.totalPrice() + " cents");
}
}Output
Books: 8000 cents
Course: 17900 centsComposite is useful when clients should treat individual objects and groups uniformly. It is not automatically a tree library: this example permits the same item to appear in multiple bundles and does not detect cycles. A cycle such as adding a bundle to itself would make recursive calculation non-terminating.
Child management methods can live on the composite only, as shown here, or on the shared component interface. The latter is more uniform but forces leaf products to reject meaningless operations such as add().
When to Use It
- When a hierarchy has leaves and containers that should support the same operation.
- When clients should calculate, render, validate, or process an entire structure without manually traversing its levels.
4. Decorator
Intent
Decorator attaches responsibilities to an object dynamically by wrapping it in another object with the same interface. Each wrapper delegates to the object it contains and may add behavior before or after that call.
The Problem It Solves
A coffee ordering system starts with espresso and later needs milk, an extra shot, and other optional features. Creating a subclass for every combination grows quickly, while putting every option into one class makes that class difficult to change. Decorators keep each feature separate and let clients compose only the options they need.
Structure
- Component:
Coffeedeclarescost_cents()anddescription(). - Concrete Component:
Espressois the base drink. - Decorator:
CoffeeDecoratorimplements the component interface and holds another coffee. - Concrete Decorators:
MilkandExtraShotadd their own cost and description.
Wrapping From Outside Inward
- The client constructs
ExtraShot(Milk(Espresso())). - Calling
cost_cents()starts at the outermost decorator and delegates inward. - Each layer adds its contribution, producing one combined cost and description.
The resulting object is still a Coffee, so it can be passed to code that does not know which options were added. The order of wrappers can matter when a feature changes behavior rather than only adding data.
Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost_cents(self) -> int:
pass
@abstractmethod
def description(self) -> str:
pass
class Espresso(Coffee):
def cost_cents(self) -> int:
return 250
def description(self) -> str:
return "Espresso"
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost_cents(self) -> int:
return self._coffee.cost_cents()
def description(self) -> str:
return self._coffee.description()
class Milk(CoffeeDecorator):
def cost_cents(self) -> int:
return self._coffee.cost_cents() + 50
def description(self) -> str:
return f"{self._coffee.description()} + milk"
class ExtraShot(CoffeeDecorator):
def cost_cents(self) -> int:
return self._coffee.cost_cents() + 80
def description(self) -> str:
return f"{self._coffee.description()} + extra shot"
if __name__ == "__main__":
order: Coffee = ExtraShot(Milk(Espresso()))
print(f"{order.description()}: {order.cost_cents()} cents")Java Implementation
// DecoratorDemo.java
interface Coffee {
int costCents();
String description();
}
final class Espresso implements Coffee {
public int costCents() { return 250; }
public String description() { return "Espresso"; }
}
abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public int costCents() { return coffee.costCents(); }
public String description() { return coffee.description(); }
}
final class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) { super(coffee); }
public int costCents() { return coffee.costCents() + 50; }
public String description() { return coffee.description() + " + milk"; }
}
final class ExtraShot extends CoffeeDecorator {
public ExtraShot(Coffee coffee) { super(coffee); }
public int costCents() { return coffee.costCents() + 80; }
public String description() { return coffee.description() + " + extra shot"; }
}
public class DecoratorDemo {
public static void main(String[] args) {
Coffee order = new ExtraShot(new Milk(new Espresso()));
System.out.println(order.description() + ": " + order.costCents() + " cents");
}
}Output
Espresso + milk + extra shot: 380 centsDecorator differs from inheritance because responsibilities attach to an instance at composition time, not to a class at compile time. It also differs from a collection of options: each decorator preserves the component contract and can alter calls, not merely store a setting.
Deep chains can become hard to debug and may duplicate wrapper logic. Keep each decorator focused, avoid dependencies on a particular wrapping order unless that order is part of the domain rule, and do not use Decorator merely to add fields to a data record.
When to Use It
- When optional responsibilities should be combined flexibly at runtime.
- When subclassing every combination would create an impractical number of classes.
5. Facade
Intent
Facade provides a simplified entry point to a set of subsystems. It coordinates a common workflow behind one interface while leaving the subsystems available for callers that need finer control.
The Problem It Solves
Placing an order requires inventory reservation, shipping scheduling, and billing. A client that calls all three services directly must know their order and dependencies. A facade gives the common case one method while keeping each subsystem independently testable and reusable.
Structure
- Facade:
OrderFacadeexposesplace_order()and coordinates the workflow. - Subsystem:
Inventory,Shipping, andBillingretain their own interfaces. - Client: uses the facade for the standard order flow.
One Entry Point, Three Coordinated Steps
- The client calls
place_order()with the order data. - The facade validates the request and invokes inventory, shipping, and billing.
- It combines the subsystem responses into one confirmation string.
This demo prints the result of each step rather than implementing persistence, compensation, or distributed transactions. A facade is not a transaction manager unless that responsibility is explicitly added and handled.
Python Implementation
from __future__ import annotations
class Inventory:
def reserve(self, sku: str, quantity: int) -> str:
return f"Reserved {quantity} x {sku}"
class Shipping:
def schedule(self, address: str) -> str:
return f"Shipping scheduled to {address}"
class Billing:
def charge(self, amount_cents: int) -> str:
return f"Billing accepted {amount_cents} cents"
class OrderFacade:
def __init__(self, inventory: Inventory, shipping: Shipping, billing: Billing):
self._inventory = inventory
self._shipping = shipping
self._billing = billing
def place_order(self, sku: str, quantity: int, address: str, amount_cents: int) -> str:
if quantity <= 0 or amount_cents < 0:
raise ValueError("Quantity and amount must be non-negative")
return " | ".join((
self._inventory.reserve(sku, quantity),
self._shipping.schedule(address),
self._billing.charge(amount_cents),
))
if __name__ == "__main__":
facade = OrderFacade(Inventory(), Shipping(), Billing())
print(facade.place_order("BOOK-42", 2, "Milan", 7600))Java Implementation
// FacadeDemo.java
final class Inventory {
public String reserve(String sku, int quantity) {
return "Reserved " + quantity + " x " + sku;
}
}
final class Shipping {
public String schedule(String address) {
return "Shipping scheduled to " + address;
}
}
final class Billing {
public String charge(int amountCents) {
return "Billing accepted " + amountCents + " cents";
}
}
final class OrderFacade {
private final Inventory inventory;
private final Shipping shipping;
private final Billing billing;
public OrderFacade(Inventory inventory, Shipping shipping, Billing billing) {
this.inventory = inventory;
this.shipping = shipping;
this.billing = billing;
}
public String placeOrder(String sku, int quantity, String address, int amountCents) {
if (quantity <= 0 || amountCents < 0) {
throw new IllegalArgumentException("Quantity and amount must be non-negative");
}
return String.join(" | ",
inventory.reserve(sku, quantity),
shipping.schedule(address),
billing.charge(amountCents));
}
}
public class FacadeDemo {
public static void main(String[] args) {
OrderFacade facade = new OrderFacade(new Inventory(), new Shipping(), new Billing());
System.out.println(facade.placeOrder("BOOK-42", 2, "Milan", 7600));
}
}Output
Reserved 2 x BOOK-42 | Shipping scheduled to Milan | Billing accepted 7600 centsA facade reduces coupling for the common workflow, but it should not become the only route to every subsystem capability. Keeping subsystem interfaces accessible avoids turning the facade into a bottleneck or a "God object."
The example returns a formatted confirmation for clarity. Production code would more likely return a structured result, distinguish partial failures, and define what happens when one subsystem fails after another has succeeded.
When to Use It
- When a common operation spans several subsystems and clients should not manage their sequencing.
- When introducing a library or legacy system behind a smaller, domain-oriented interface.
6. Flyweight
Intent
Flyweight shares immutable state that repeats across many objects. Each object keeps only its unique state and refers to a shared flyweight for the common part.
The Problem It Solves
A forest may contain millions of trees. Storing the species name, color, and texture reference in every tree repeats the same data. A shared immutable TreeType reduces that repetition, while each tree retains only its own position and height.
Structure
- Flyweight:
TreeTypeis immutable and contains intrinsic state shared by many trees. - Flyweight Factory:
TreeTypeFactorymaps a key to one cached instance. - Context:
Treestores extrinsic state: x, y, and height. - Client:
Forestrequests a shared type and creates contextual tree objects.
Separating Shared and Unique State
- The forest requests a tree type using name, color, and texture.
- The factory returns the existing cached type or creates it once.
- Each tree stores a reference to that type plus its unique coordinates and height.
Three trees use only two shared types in the demo. The savings grow with the number of objects and the size of the shared state.
Python Implementation
from __future__ import annotations
from dataclasses import dataclass
from typing import ClassVar
@dataclass(frozen=True)
class TreeType:
name: str
color: str
texture: str
class TreeTypeFactory:
_types: ClassVar[dict[tuple[str, str, str], TreeType]] = {}
@classmethod
def get(cls, name: str, color: str, texture: str) -> TreeType:
key = (name, color, texture)
if key not in cls._types:
cls._types[key] = TreeType(name, color, texture)
return cls._types[key]
class Tree:
def __init__(self, tree_type: TreeType, x: float, y: float, height: float):
self.type = tree_type
self.x = x
self.y = y
self.height = height
def draw(self) -> str:
return f"{self.type.name} at ({self.x:.1f}, {self.y:.1f}), height={self.height:.1f}"
class Forest:
def __init__(self):
self.trees: list[Tree] = []
def plant(self, name: str, color: str, texture: str, x: float, y: float, height: float) -> Tree:
tree = Tree(TreeTypeFactory.get(name, color, texture), x, y, height)
self.trees.append(tree)
return tree
if __name__ == "__main__":
forest = Forest()
forest.plant("Oak", "green", "oak.png", 10, 20, 4.5)
forest.plant("Oak", "green", "oak.png", 15, 25, 3.8)
forest.plant("Pine", "dark green", "pine.png", 30, 40, 6.0)
print(f"Trees planted: {len(forest.trees)}")
print(f"Shared types: {len(TreeTypeFactory._types)}")
print(forest.trees[0].draw())
print(forest.trees[2].draw())Java Implementation
// FlyweightDemo.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
final class TreeType {
private final String name;
private final String color;
private final String texture;
private TreeType(String name, String color, String texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
public static TreeType of(String name, String color, String texture) {
return new TreeType(name, color, texture);
}
public String getName() { return name; }
}
final class TreeTypeFactory {
private static final Map<String, TreeType> TYPES = new HashMap<>();
public static TreeType get(String name, String color, String texture) {
String key = String.join("|", name, color, texture);
return TYPES.computeIfAbsent(key, ignored -> TreeType.of(name, color, texture));
}
public static int typeCount() { return TYPES.size(); }
}
final class Tree {
private final TreeType type;
private final double x;
private final double y;
private final double height;
public Tree(TreeType type, double x, double y, double height) {
this.type = Objects.requireNonNull(type);
this.x = x;
this.y = y;
this.height = height;
}
public String draw() {
return String.format(Locale.ROOT, "%s at (%.1f, %.1f), height=%.1f",
type.getName(), x, y, height);
}
}
final class Forest {
private final List<Tree> trees = new ArrayList<>();
public Tree plant(String name, String color, String texture, double x, double y, double height) {
Tree tree = new Tree(TreeTypeFactory.get(name, color, texture), x, y, height);
trees.add(tree);
return tree;
}
public int treeCount() { return trees.size(); }
}
public class FlyweightDemo {
public static void main(String[] args) {
Forest forest = new Forest();
forest.plant("Oak", "green", "oak.png", 10, 20, 4.5);
forest.plant("Oak", "green", "oak.png", 15, 25, 3.8);
forest.plant("Pine", "dark green", "pine.png", 30, 40, 6.0);
System.out.println("Trees planted: " + forest.treeCount());
System.out.println("Shared types: " + TreeTypeFactory.typeCount());
System.out.println(forest.plant("Oak", "green", "oak.png", 50, 60, 5.0).draw());
}
}Output
Trees planted: 3
Shared types: 2
Oak at (10.0, 20.0), height=4.5
Pine at (30.0, 40.0), height=6.0Intrinsic state must be safe to share. Here, TreeType is immutable and contains no identity-dependent behavior. Extrinsic state remains with each Tree, so two trees can share a type while occupying different positions.
The cache key must identify the shared state completely. Using only the species name would incorrectly share different colors or textures. A bounded or evictable cache may be appropriate when the set of flyweights is large or changes over time.
When to Use It
- When many objects repeat substantial immutable state.
- When object identity of the shared part does not matter and its state can be safely reused.
7. Proxy
Intent
Proxy provides a substitute with the same interface as another object. It can control, delay, cache, log, or protect access before forwarding a request to the real subject.
The Problem It Solves
A report service may expose confidential documents. Clients should use one reporting interface, but access rules should be enforced before the real service performs its work. A proxy can check the caller role and forward only authorized requests.
Structure
- Subject:
ReportServicedeclaresread(report_id). - Real Subject:
RealReportServiceperforms the actual data access. - Proxy:
AccessControlProxyimplements the same interface and controls delegation. - Client: depends on the subject interface, not directly on the real service.
Authorize, Then Delegate
- The client calls
read()on the proxy. - The proxy checks whether the caller role is allowed.
- An authorized request reaches the real service; an unauthorized request raises an access error.
The Python proxy accepts a role argument for clarity. The Java proxy uses a fixed guest role to retain the same subject signature. Both make the access decision before touching the real service.
Python Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
class ReportService(ABC):
@abstractmethod
def read(self, report_id: str) -> str:
pass
class RealReportService(ReportService):
def read(self, report_id: str) -> str:
return f"Contents of {report_id}"
class AccessControlProxy(ReportService):
def __init__(self, service: ReportService, allowed_roles: set[str]):
self._service = service
self._allowed_roles = allowed_roles
def read(self, report_id: str, role: str = "guest") -> str:
if role not in self._allowed_roles:
raise PermissionError(f"Role '{role}' cannot read {report_id}")
return self._service.read(report_id)
if __name__ == "__main__":
service = AccessControlProxy(RealReportService(), {"analyst", "admin"})
print(service.read("quarterly-report", "analyst"))
try:
service.read("quarterly-report", "guest")
except PermissionError as error:
print(error)Java Implementation
// ProxyDemo.java
interface ReportService {
String read(String reportId);
}
final class RealReportService implements ReportService {
public String read(String reportId) {
return "Contents of " + reportId;
}
}
final class AccessControlProxy implements ReportService {
private final ReportService service;
private final java.util.Set<String> allowedRoles;
public AccessControlProxy(ReportService service, java.util.Set<String> allowedRoles) {
this.service = service;
this.allowedRoles = java.util.Set.copyOf(allowedRoles);
}
public String read(String reportId) {
String role = "guest";
if (!allowedRoles.contains(role)) {
throw new SecurityException("Role '" + role + "' cannot read " + reportId);
}
return service.read(reportId);
}
}
public class ProxyDemo {
public static void main(String[] args) {
ReportService service = new AccessControlProxy(
new RealReportService(), java.util.Set.of("analyst", "admin"));
System.out.println(service.read("quarterly-report"));
try {
new AccessControlProxy(new RealReportService(), java.util.Set.of("admin"))
.read("quarterly-report");
} catch (SecurityException error) {
System.out.println(error.getMessage());
}
}
}Output
Contents of quarterly-report
Role 'guest' cannot read quarterly-reportProtection, virtual, caching, logging, and remote proxies share the same structural idea but solve different access problems. The important contract is interface compatibility: a client should be able to use the proxy wherever it could use the real subject.
A proxy adds a layer and can change latency or failure behavior. Make its policy explicit, especially for caching, retries, and security decisions. This demo does not authenticate the caller; it assumes the supplied role is already trusted.
When to Use It
- When access to an expensive, remote, or protected resource needs a controlled boundary.
- When cross-cutting behavior should surround a service without modifying the service implementation.
8. Detailed Comparison
Pattern Selection Guide
Identify the composition pressure before selecting a pattern. The table below distinguishes the boundary each pattern introduces and the contract that must be made explicit.
| Design Pressure | Pattern | Composition Boundary | What Must Be Defined | Avoid It When |
|---|---|---|---|---|
| An existing interface does not match the client | Adapter | Adapter translates between Target and Adaptee | Unit, protocol, and error mappings | You can change the incompatible interface directly |
| Two independent dimensions would multiply subclasses | Bridge | Abstraction holds an Implementation reference | Which variations belong to each hierarchy | There is only one stable implementation |
| Clients should treat leaves and groups alike | Composite | Composite stores child Components | Cycle policy and child-management API | The structure is fixed and shallow |
| Optional behavior must compose at runtime | Decorator | Decorator wraps another Component | Delegation order and added responsibilities | A few constructor parameters express the options clearly |
| A common workflow spans several subsystems | Facade | Facade coordinates subsystem calls | Workflow order, validation, and failure policy | Clients need unrestricted access to every subsystem feature |
| Many objects repeat immutable state | Flyweight | Factory caches shared Flyweight objects | Cache key, sharing safety, and extrinsic state | The repeated state is small or object identity matters |
| Access to a subject needs a controlled boundary | Proxy | Proxy implements the Subject interface | Authorization, caching, retry, and failure behavior | Direct access is simpler and has no cross-cutting requirement |
Resolve the Closest Choices
- Adapter or Facade: Adapter reconciles an existing incompatible interface; Facade simplifies access to several already-usable subsystems.
- Bridge or Adapter: Bridge plans two independent variation axes; Adapter connects interfaces that already exist but do not match.
- Decorator or Proxy: Both wrap a subject, but Decorator adds responsibilities while Proxy controls access, creation, or location.
- Composite or Decorator: Composite forms a tree of uniformly treated components; Decorator wraps one component to add behavior.
- Flyweight or shared immutable object: use Flyweight when many contexts need the same intrinsic state and can supply their own extrinsic state.
Common Pitfalls
| Pattern | Common Pitfall | Better Approach |
|---|---|---|
| Adapter | Hiding incompatible semantics behind a renamed method | Document conversions, validation, and error translation explicitly |
| Bridge | Creating a hierarchy split without two genuine variation axes | Use Bridge only when abstraction and implementation must evolve independently |
| Composite | Allowing cycles or treating every node as both leaf and container | Keep leaf and composite responsibilities clear; define cycle handling |
| Decorator | Building deep, order-dependent wrapper chains | Keep wrappers small and make any required ordering explicit |
| Facade | Turning the facade into a "God object" that owns every capability | Coordinate common flows while preserving direct subsystem access |
| Flyweight | Sharing mutable state or using an incomplete cache key | Make shared state immutable and include every distinguishing field in the key |
| Proxy | Assuming interface compatibility makes behavior transparent | Document latency, caching, security, and error-handling changes |
9. Putting It All Together
A structural pattern earns its place when it clarifies a real composition boundary. It should make a variation, access rule, or hierarchy easier to understand, not merely add another layer of indirection.
Composition Contracts Worth Making Explicit
- Interface: what can clients rely on, and which implementation details remain hidden behind it?
- Ownership: which object owns mutable state, and which parts are safe to share or replace?
- Delegation: when is a call forwarded, transformed, rejected, cached, or coordinated across collaborators?
Prefer direct composition when it expresses the design clearly. Introduce a structural pattern when it isolates a change that would otherwise spread through client code, and keep the resulting boundary small, documented, and testable.
Pattern terminology and relationships can be explored in the Structural Patterns catalog; the examples here focus on explicit composition, delegation, and runtime behavior.