Structural Patterns: Object Composition and Interface Design
Adapter, Facade, Decorator, Proxy, Composite, Bridge, plus the Sidecar and BFF architecture patterns
Contents
Structural patterns compose objects to reconcile interfaces and hide complexity.
Overview
Structural patterns describe how classes and objects are composed into larger structures. They reconcile incompatible interfaces and give clients a simple way into a complicated subsystem. Adding new behavior to an existing object at runtime also falls under structural patterns. In the microservices era the same ideas were lifted up into architecture patterns that structure communication between services.
Adapter pattern
Definition
The Adapter converts an incompatible interface so that two parties can work together. It is also called a Wrapper.
A real case: integrating a legacy payment system
// Legacy payment interface of the existing system
public class LegacyPaymentSystem {
public boolean processPayment(String cardNumber, double amount, String currency) {
// legacy logic...
return true;
}
}
// The new standard interface
public interface PaymentProcessor {
PaymentResult charge(PaymentRequest request);
}
// Adapter: fits the legacy system to the new interface
public class LegacyPaymentAdapter implements PaymentProcessor {
private final LegacyPaymentSystem legacySystem;
public LegacyPaymentAdapter(LegacyPaymentSystem legacySystem) {
this.legacySystem = legacySystem;
}
@Override
public PaymentResult charge(PaymentRequest request) {
boolean success = legacySystem.processPayment(
request.getCardNumber(),
request.getAmount().doubleValue(),
request.getCurrency().getCode()
);
return new PaymentResult(success, success ? "OK" : "FAILED");
}
}Two kinds of Adapter
| Kind | Mechanism | Characteristics |
|---|---|---|
| Object Adapter | Composition | Flexible, and the recommended form |
| Class Adapter | Inheritance | Needs multiple inheritance, so it is limited |
Where it is used
Use an Adapter when the existing code cannot be modified, or when changing either interface is impractical, and slot it in between the two.
- External API integration (wrapping a third-party library)
- Legacy system migration (incremental replacement)
- Data format conversion (XML to JSON and back)
- ORM adapters (compatibility across MySQL, PostgreSQL, SQLite and other drivers)
Adapter and hexagonal architecture
In hexagonal architecture the Adapter is a core building block:
┌─────────────────────────────────────────┐
│ Application Core │
│ ┌─────────────────────────────────┐ │
│ │ Domain Logic (Ports) │ │
│ └─────────┬───────────┬──────────┘ │
│ │ │ │
│ ┌─────────▼──┐ ┌─────▼──────────┐ │
│ │ Driving │ │ Driven │ │
│ │ Adapter │ │ Adapter │ │
│ │ (REST API) │ │ (PostgreSQL) │ │
│ └────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘- Driving Adapter: connects an external request to an inbound port (REST controller, gRPC handler)
- Driven Adapter: connects an outbound port to an external system (database repository, message publisher)
Facade pattern
Definition
The Facade provides a single simplified interface over a complicated subsystem. The client uses the core functionality without needing to know how the subsystem is put together.
Example: an order processing facade
// The complicated subsystems
public class InventoryService { /* inventory management */ }
public class PaymentService { /* payment processing */ }
public class ShippingService { /* shipping management */ }
public class NotificationService { /* notification delivery */ }
public class FraudDetectionService { /* fraud detection */ }
// Facade: exposes the complicated order process through one simple interface
public class OrderFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final ShippingService shipping;
private final NotificationService notification;
private final FraudDetectionService fraud;
public OrderFacade(InventoryService inventory, PaymentService payment,
ShippingService shipping, NotificationService notification,
FraudDetectionService fraud) {
this.inventory = inventory;
this.payment = payment;
this.shipping = shipping;
this.notification = notification;
this.fraud = fraud;
}
// The client only ever calls this method
public OrderResult placeOrder(OrderRequest request) {
// 1. fraud detection
fraud.validate(request);
// 2. check and reserve inventory
inventory.reserve(request.getItems());
// 3. process the payment
PaymentResult paymentResult = payment.charge(request.getPayment());
// 4. request shipping
ShippingLabel label = shipping.createLabel(request.getAddress());
// 5. send the notification
notification.sendOrderConfirmation(request.getCustomer());
return new OrderResult(paymentResult, label);
}
}API Gateway: the Facade of microservices
In a microservice architecture the API Gateway is the Facade pattern applied at the architecture level:
Client ──→ [API Gateway (Facade)] ──→ User Service
──→ Order Service
──→ Payment Service
──→ Inventory ServiceWhat an API Gateway is responsible for:
| Responsibility | Description |
|---|---|
| Routing | Forward a request to the right service |
| Aggregation | Compose responses from several services into one |
| Auth | Handle authentication and authorization centrally |
| Rate Limiting | Cap requests per service |
| Protocol translation | Convert between REST, gRPC, WebSocket and so on |
Common implementations: Kong, AWS API Gateway, Envoy, NGINX
Decorator pattern
Definition
The Decorator adds new responsibilities to an existing object at runtime. It is the alternative to inheritance, and it is more flexible because behavior can be composed while the program runs.
Example: logging, caching and retry
// The base interface
public interface DataService {
Data fetch(String key);
}
// The base implementation
public class RemoteDataService implements DataService {
public Data fetch(String key) {
return callRemoteApi(key);
}
}
// Decorator: adds logging
public class LoggingDecorator implements DataService {
private final DataService delegate;
private final Logger logger;
public LoggingDecorator(DataService delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
public Data fetch(String key) {
logger.info("Fetching data for key: {}", key);
long start = System.nanoTime();
Data result = delegate.fetch(key);
long elapsed = System.nanoTime() - start;
logger.info("Fetched in {}ms", elapsed / 1_000_000);
return result;
}
}
// Decorator: adds caching
public class CachingDecorator implements DataService {
private final DataService delegate;
private final Cache cache;
public CachingDecorator(DataService delegate, Cache cache) {
this.delegate = delegate;
this.cache = cache;
}
public Data fetch(String key) {
Data cached = cache.get(key);
if (cached != null) return cached;
Data result = delegate.fetch(key);
cache.put(key, result);
return result;
}
}
// Decorator: adds retry
public class RetryDecorator implements DataService {
private final DataService delegate;
private final int maxRetries;
public RetryDecorator(DataService delegate, int maxRetries) {
this.delegate = delegate;
this.maxRetries = maxRetries;
}
public Data fetch(String key) {
for (int i = 0; i < maxRetries; i++) {
try {
return delegate.fetch(key);
} catch (Exception e) {
if (i == maxRetries - 1) throw e;
}
}
throw new RuntimeException("Unreachable");
}
}
// Composition: logging -> caching -> retry -> the real call
DataService service = new LoggingDecorator(
new CachingDecorator(
new RetryDecorator(
new RemoteDataService(), 3
), redisCache
), logger
);Decorator versus inheritance
| Aspect | Decorator | Inheritance |
|---|---|---|
| When behavior is added | Runtime (dynamic) | Compile time (static) |
| Composability | Combine freely | Class explosion |
| Open-closed principle | Respected | Easily violated |
| Complexity | Nesting can get deep | The inheritance tree can get deep |
In practice
- Java I/O streams:
BufferedReader(FileReader(file)) - HTTP middleware: the middleware chains in Express.js and Koa
- Aspect-Oriented Programming (AOP): proxy-based decoration in Spring AOP
Proxy pattern
Definition
The Proxy provides a surrogate object that controls access to another object.
Kinds of Proxy
| Kind | Purpose | Example |
|---|---|---|
| Virtual Proxy | Lazy loading | Deferring the load of a large image |
| Protection Proxy | Access control | Restricting method calls by role |
| Remote Proxy | Local stand-in for a remote object | Remote Procedure Call (RPC) or gRPC stub |
| Caching Proxy | Caching results | Storing the result of repeated calls |
| Logging Proxy | Recording requests | Method call logs |
Example: a protection proxy
public interface UserRepository {
User findById(String id);
void delete(String id);
}
// Access control proxy
public class SecuredUserRepository implements UserRepository {
private final UserRepository delegate;
private final SecurityContext securityContext;
public SecuredUserRepository(UserRepository delegate,
SecurityContext securityContext) {
this.delegate = delegate;
this.securityContext = securityContext;
}
@Override
public User findById(String id) {
// reads only require authentication
securityContext.requireAuthentication();
return delegate.findById(id);
}
@Override
public void delete(String id) {
// deletes require the ADMIN role
securityContext.requireRole("ADMIN");
delegate.delete(id);
}
}Java Dynamic Proxy and CGLIB
The Spring Framework implements AOP internally with dynamic proxies and CGLIB:
// Spring AOP: @Transactional is implemented with a proxy
@Service
public class OrderService {
@Transactional // the proxy begins and ends the transaction around this call
public void createOrder(OrderRequest request) {
// only the business logic lives here
}
}Composite pattern
Definition
The Composite arranges objects into a tree to express a part-whole hierarchy. Individual objects and composites are treated uniformly.
Example: a file system
public interface FileSystemNode {
String getName();
long getSize();
void print(String indent);
}
public class File implements FileSystemNode {
private final String name;
private final long size;
public String getName() { return name; }
public long getSize() { return size; }
public void print(String indent) {
System.out.println(indent + name + " (" + size + "B)");
}
}
public class Directory implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public void add(FileSystemNode node) { children.add(node); }
public String getName() { return name; }
public long getSize() {
return children.stream()
.mapToLong(FileSystemNode::getSize)
.sum(); // recursively computes the total size
}
public void print(String indent) {
System.out.println(indent + name + "/");
children.forEach(child -> child.print(indent + " "));
}
}Where it is used
- UI component trees (React virtual DOM, Swing)
- Org charts (division to team to individual)
- Menu systems (menu to submenu to menu item)
- Query builders (a tree of AND/OR conditions)
Bridge pattern
Definition
The Bridge separates an abstraction from its implementation so that each can change independently.
Example: a notification system
// Implementation layer: delivery channels
public interface NotificationChannel {
void send(String recipient, String content);
}
public class EmailChannel implements NotificationChannel { /* ... */ }
public class SlackChannel implements NotificationChannel { /* ... */ }
public class SmsChannel implements NotificationChannel { /* ... */ }
// Abstraction layer: notification types
public abstract class Notification {
protected final NotificationChannel channel;
public Notification(NotificationChannel channel) {
this.channel = channel;
}
public abstract void notify(String recipient, Map<String, Object> data);
}
public class UrgentNotification extends Notification {
public UrgentNotification(NotificationChannel channel) {
super(channel);
}
public void notify(String recipient, Map<String, Object> data) {
String content = "[URGENT] " + formatMessage(data);
channel.send(recipient, content);
}
}
public class ScheduledNotification extends Notification {
// scheduled delivery logic...
}What the Bridge buys
- Notification types (urgent, scheduled, normal) and delivery channels (email, Slack, SMS) grow independently
- An M x N combination problem shrinks to M + N
- The implementation can be swapped at runtime
Structural patterns in the microservices era
Sidecar pattern
The Sidecar places a helper process alongside a microservice to handle cross-cutting concerns.
Cross-cutting concerns the sidecar takes on:
- Service discovery
- Load balancing
- Encryption and TLS termination (mutual TLS, mTLS)
- Metrics collection (observability)
- Circuit breaking
- Retry and timeout
Service mesh: an infrastructure layer that applies the Sidecar pattern across an entire cluster
| Service mesh | Sidecar proxy | Characteristics |
|---|---|---|
| Istio | Envoy | Richest feature set, highest complexity |
| Linkerd | linkerd2-proxy (Rust) | Lightweight, minimal configuration |
| Consul Connect | Envoy | Integrated with the HashiCorp ecosystem |
| Cilium | Based on extended Berkeley Packet Filter (eBPF) | Handled in the kernel with no sidecar |
Sidecar-less service meshes built on eBPF (Cilium, Istio Ambient Mesh) are gaining ground as a way to cut the performance overhead of the sidecar approach.
BFF pattern (Backend for Frontend)
The BFF gives each client type its own dedicated backend service. It is the interface segregation principle (ISP) applied at the architecture level.
Mobile App ──→ [Mobile BFF] ──→ User Service
──→ Order Service (lightweight response)
Web App ────→ [Web BFF] ──→ User Service
──→ Order Service (detailed response)
──→ Analytics Service
Admin ──────→ [Admin BFF] ──→ User Service (admin operations)
──→ Audit ServiceWhat the BFF gives you:
- API responses tuned per client (mobile: lightweight, web: detailed)
- Frontend teams can change their own BFF independently
- ISP from SOLID realized at the architecture level
- Even more flexibility when combined with GraphQL Federation
Structural pattern comparison
| Pattern | Purpose | Core mechanism | Typical use |
|---|---|---|---|
| Adapter | Interface compatibility | Wrapping | Legacy integration, external APIs |
| Facade | Hiding complexity | Unified interface | API Gateway, SDKs |
| Decorator | Adding behavior at runtime | Nested wrapping | Middleware, I/O streams |
| Proxy | Access control | Surrogate object | AOP, lazy loading |
| Composite | Tree structure | Recursive composition | UI, file systems |
| Bridge | Abstraction-implementation split | Two layers | Notifications, drivers |
| Sidecar | Isolating cross-cutting concerns | Helper process | Service mesh |
| BFF | Per-client optimization | Dedicated backend | Multi-platform APIs |
Summary
Structural patterns compose objects to solve interface compatibility, complexity hiding, runtime behavior extension, and access control. Adapter and Facade either reconcile or conceal an interface, while Decorator and Proxy wrap an existing object to manage behavior and access. Composite and Bridge split the structure itself into a tree or into two independent layers. The same composition principle scales up in microservices as the API Gateway, Sidecar, and BFF patterns. Asking which kind of change you want to isolate, rather than which pattern name fits, makes the choice much clearer.
The next post covers behavioral patterns.