jongkwan.dev
Development · Essay №006

Structural Patterns: Object Composition and Interface Design

Adapter, Facade, Decorator, Proxy, Composite, Bridge, plus the Sidecar and BFF architecture patterns

Jongkwan Lee2025년 3월 16일12 min read
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

java
// 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

KindMechanismCharacteristics
Object AdapterCompositionFlexible, and the recommended form
Class AdapterInheritanceNeeds 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:

text
┌─────────────────────────────────────────┐
│              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

java
// 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:

text
Client ──→ [API Gateway (Facade)] ──→ User Service
                                  ──→ Order Service
                                  ──→ Payment Service
                                  ──→ Inventory Service

What an API Gateway is responsible for:

ResponsibilityDescription
RoutingForward a request to the right service
AggregationCompose responses from several services into one
AuthHandle authentication and authorization centrally
Rate LimitingCap requests per service
Protocol translationConvert 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

java
// 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

AspectDecoratorInheritance
When behavior is addedRuntime (dynamic)Compile time (static)
ComposabilityCombine freelyClass explosion
Open-closed principleRespectedEasily violated
ComplexityNesting can get deepThe 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

KindPurposeExample
Virtual ProxyLazy loadingDeferring the load of a large image
Protection ProxyAccess controlRestricting method calls by role
Remote ProxyLocal stand-in for a remote objectRemote Procedure Call (RPC) or gRPC stub
Caching ProxyCaching resultsStoring the result of repeated calls
Logging ProxyRecording requestsMethod call logs

Example: a protection proxy

java
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:

java
// 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

java
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

java
// 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 meshSidecar proxyCharacteristics
IstioEnvoyRichest feature set, highest complexity
Linkerdlinkerd2-proxy (Rust)Lightweight, minimal configuration
Consul ConnectEnvoyIntegrated with the HashiCorp ecosystem
CiliumBased 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.

text
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 Service

What 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

PatternPurposeCore mechanismTypical use
AdapterInterface compatibilityWrappingLegacy integration, external APIs
FacadeHiding complexityUnified interfaceAPI Gateway, SDKs
DecoratorAdding behavior at runtimeNested wrappingMiddleware, I/O streams
ProxyAccess controlSurrogate objectAOP, lazy loading
CompositeTree structureRecursive compositionUI, file systems
BridgeAbstraction-implementation splitTwo layersNotifications, drivers
SidecarIsolating cross-cutting concernsHelper processService mesh
BFFPer-client optimizationDedicated backendMulti-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.