Compile Ready
All low level design problems
Low Level Design/Concepts & Foundations/ Concept

Structural Patterns

Adapter, Decorator, Facade, Composite, Proxy, Bridge, and Flyweight — how to compose objects into larger structures.

6m readPatternsStructuralDecoratorAdapterProxy

Introduction

Structural design patterns explain how classes and objects are assembled into larger shapes without turning every class into a tightly coupled dependency graph.

In LLD interviews at Amazon, Microsoft, and Netflix, these patterns show up whenever the design has third-party integrations, optional features, tree-shaped data, expensive objects, remote resources, or multiple dimensions of variation. The core skill is not naming the pattern. The core skill is recognizing the coupling problem and choosing the smallest structure that keeps the design extensible.

Learning Objectives

  • Explain the intent of Adapter, Decorator, Facade, Composite, Proxy, Bridge, and Flyweight.

  • Write compact Java examples that demonstrate each structural pattern.

  • Map each pattern to real production use cases and common LLD interview problems.

  • Compare Decorator, Proxy, and Adapter by the kind of boundary they manage.

  • Compare Bridge and Adapter by whether variation is designed up front or repaired later.

  • Use structural patterns without hiding domain rules inside accidental wrappers.

Core Theory

Structural patterns organize object relationships

A structural pattern should answer one of these questions:

  • How do I make an incompatible class usable without changing it?
  • How do I add behavior without subclass explosion?
  • How do I hide a complex subsystem behind a simple API?
  • How do I represent part-whole hierarchies uniformly?
  • How do I control access to a real object?
  • How do I separate abstraction from implementation when both vary?
  • How do I share many small immutable objects efficiently?

In interviews, introduce the pattern only after explaining the design pressure. That makes the pattern feel like a consequence of requirements, not memorized vocabulary.

Adapter: make incompatible interfaces work together

Intent: Convert the interface of an existing class into the interface your domain expects, without changing the existing class.

Real use case: A checkout service exposes one PaymentGateway contract while Stripe, Razorpay, PayPal, or a bank SDK has different method names and request objects.

In LLD problems: Use Adapter when your clean domain model must talk to a legacy class, third-party SDK, vendor API, or old internal module. In a notification system, adapters can normalize email, SMS, and push providers behind one NotificationChannel contract.

public interface PaymentGateway {
    PaymentReceipt charge(String orderId, long amountInCents);
}

public final class LegacyBankClient {
    public String makePayment(String reference, int amountInPaise) {
        return "BANK-" + reference + "-" + amountInPaise;
    }
}

public final class BankPaymentAdapter implements PaymentGateway {
    private final LegacyBankClient legacyClient;

    public BankPaymentAdapter(LegacyBankClient legacyClient) {
        this.legacyClient = legacyClient;
    }

    @Override
    public PaymentReceipt charge(String orderId, long amountInCents) {
        String bankId = legacyClient.makePayment(orderId, Math.toIntExact(amountInCents));
        return new PaymentReceipt(orderId, bankId, true);
    }
}

public record PaymentReceipt(String orderId, String providerReference, boolean success) {
}

Decorator: add responsibilities around the same contract

Intent: Wrap an object with another object that implements the same interface, adding behavior before or after delegating to the wrapped object.

Real use case: HTTP clients are commonly decorated with retry, metrics, tracing, compression, authentication, and rate limiting without changing the base client.

In LLD problems: Use Decorator when features are optional and composable. In coffee machine, toppings decorate a base beverage. In logging framework, timestamp, masking, sampling, or async behavior can decorate a logger while preserving the Logger interface.

public interface Notifier {
    void send(String userId, String message);
}

public final class EmailNotifier implements Notifier {
    @Override
    public void send(String userId, String message) {
        System.out.println("email to " + userId + ": " + message);
    }
}

public abstract class NotifierDecorator implements Notifier {
    private final Notifier delegate;

    protected NotifierDecorator(Notifier delegate) {
        this.delegate = delegate;
    }

    @Override
    public void send(String userId, String message) {
        delegate.send(userId, message);
    }
}

public final class SmsNotifierDecorator extends NotifierDecorator {
    public SmsNotifierDecorator(Notifier delegate) {
        super(delegate);
    }

    @Override
    public void send(String userId, String message) {
        super.send(userId, message);
        System.out.println("sms to " + userId + ": " + message);
    }
}

Facade: provide a simple entry point over a subsystem

Intent: Offer a coarse-grained API that hides the coordination details of several collaborating classes.

Real use case: A media upload service can expose uploadVideo while internally validating metadata, storing the raw file, transcoding variants, generating thumbnails, and publishing events.

In LLD problems: Use Facade when the client should not orchestrate many subsystem calls. In hotel booking, a BookingFacade can coordinate room inventory, pricing, payment, and confirmation without making controllers know every service.

public final class OrderFacade {
    private final InventoryService inventory;
    private final PaymentService payments;
    private final ShipmentService shipments;

    public OrderFacade(InventoryService inventory, PaymentService payments, ShipmentService shipments) {
        this.inventory = inventory;
        this.payments = payments;
        this.shipments = shipments;
    }

    public OrderConfirmation placeOrder(String sku, int quantity, long amountInCents) {
        inventory.reserve(sku, quantity);
        String paymentId = payments.charge(amountInCents);
        String shipmentId = shipments.createShipment(sku, quantity);
        return new OrderConfirmation(paymentId, shipmentId);
    }
}

public interface InventoryService { void reserve(String sku, int quantity); }
public interface PaymentService { String charge(long amountInCents); }
public interface ShipmentService { String createShipment(String sku, int quantity); }
public record OrderConfirmation(String paymentId, String shipmentId) { }

Composite: treat individual objects and groups uniformly

Intent: Compose objects into tree structures and let callers use leaves and groups through the same component interface.

Real use case: File systems, menus, organization charts, product bundles, expression trees, and nested comments all have part-whole structure.

In LLD problems: Use Composite when the problem has folders and files, tasks and subtasks, categories and products, or comments and replies. It lets traversal, pricing, permission checks, and rendering work recursively without type checks at every level.

import java.util.ArrayList;
import java.util.List;

public interface FileSystemNode {
    String name();
    long sizeInBytes();
}

public final class FileNode implements FileSystemNode {
    private final String name;
    private final long sizeInBytes;

    public FileNode(String name, long sizeInBytes) {
        this.name = name;
        this.sizeInBytes = sizeInBytes;
    }

    public String name() { return name; }
    public long sizeInBytes() { return sizeInBytes; }
}

public final class FolderNode implements FileSystemNode {
    private final String name;
    private final List<FileSystemNode> children = new ArrayList<>();

    public FolderNode(String name) {
        this.name = name;
    }

    public void add(FileSystemNode child) { children.add(child); }
    public String name() { return name; }

    public long sizeInBytes() {
        return children.stream().mapToLong(FileSystemNode::sizeInBytes).sum();
    }
}

Proxy: control access to another object

Intent: Stand in front of a real subject and control access, lifecycle, caching, security, rate limiting, or remote communication while preserving the same interface.

Real use case: Repository proxies add caching, service proxies hide network calls, image proxies lazy-load heavy images, and authorization proxies check permissions before invoking a protected service.

In LLD problems: Use Proxy when callers should depend on the same contract but access must be mediated. In a rate limiter, a proxy can guard an API client. In an authentication service, a proxy can check tokens before delegating to account operations.

import java.util.HashMap;
import java.util.Map;

public interface UserDirectory {
    UserProfile findById(String userId);
}

public final class DatabaseUserDirectory implements UserDirectory {
    @Override
    public UserProfile findById(String userId) {
        return new UserProfile(userId, "loaded from database");
    }
}

public final class CachedUserDirectoryProxy implements UserDirectory {
    private final UserDirectory delegate;
    private final Map<String, UserProfile> cache = new HashMap<>();

    public CachedUserDirectoryProxy(UserDirectory delegate) {
        this.delegate = delegate;
    }

    @Override
    public UserProfile findById(String userId) {
        return cache.computeIfAbsent(userId, delegate::findById);
    }
}

public record UserProfile(String userId, String source) {
}

Bridge: separate abstraction from implementation

Intent: Split a hierarchy into two dimensions so the abstraction and implementation can vary independently.

Real use case: A notification abstraction such as Alert can vary by alert type while delivery varies by email, SMS, Slack, or push. Adding a new alert should not require subclasses for every channel combination.

In LLD problems: Use Bridge when you see two independent axes such as device and remote, shape and renderer, message type and channel, payment flow and provider, or report and exporter. It avoids multiplying classes like UrgentEmailAlert, UrgentSmsAlert, and DigestEmailAlert.

public interface MessageSender {
    void send(String destination, String body);
}

public final class EmailSender implements MessageSender {
    @Override
    public void send(String destination, String body) {
        System.out.println("email " + destination + ": " + body);
    }
}

public abstract class Alert {
    private final MessageSender sender;

    protected Alert(MessageSender sender) {
        this.sender = sender;
    }

    protected void deliver(String destination, String body) {
        sender.send(destination, body);
    }

    public abstract void notify(String destination);
}

public final class CriticalAlert extends Alert {
    public CriticalAlert(MessageSender sender) {
        super(sender);
    }

    @Override
    public void notify(String destination) {
        deliver(destination, "critical incident detected");
    }
}

Flyweight: share intrinsic state across many objects

Intent: Reuse immutable shared state for many fine-grained objects and keep unique extrinsic state outside the shared object.

Real use case: Text editors share glyph metadata, maps share marker icons, games share sprites, and inventory systems share product definitions while each occurrence stores only position, quantity, or owner-specific state.

In LLD problems: Use Flyweight when the design creates thousands or millions of similar objects. In chess or ludo, piece type metadata can be shared. In a text editor, each character occurrence can reference a shared glyph style instead of duplicating font data.

import java.util.HashMap;
import java.util.Map;

public final class ProductType {
    private final String sku;
    private final String name;
    private final int weightGrams;

    public ProductType(String sku, String name, int weightGrams) {
        this.sku = sku;
        this.name = name;
        this.weightGrams = weightGrams;
    }

    public String sku() { return sku; }
    public String name() { return name; }
    public int weightGrams() { return weightGrams; }
}

public final class ProductTypeFactory {
    private final Map<String, ProductType> cache = new HashMap<>();

    public ProductType get(String sku, String name, int weightGrams) {
        return cache.computeIfAbsent(sku, key -> new ProductType(key, name, weightGrams));
    }
}

public record InventoryItem(String binId, int quantity, ProductType type) {
}

Diagrams

Composite pattern for a file system

Rendering diagram…
FileNode and FolderNode share the FileSystemNode contract. FolderNode stores children of the same contract, so size calculation and traversal can be recursive.

Comparisons

Decorator vs Proxy vs Adapter

DimensionDecoratorProxyAdapterDecision clue
Primary purposeAdd behavior around the same contractControl access to a real subjectConvert one interface into anotherAsk whether the issue is extra behavior, mediated access, or incompatible API
Interface shapeSame as wrapped componentSame as real subjectTarget interface differs from adaptee interfaceAdapter is the only one whose main job is interface translation
Typical behaviorRetry, metrics, compression, masking, extra notification channelCaching, lazy loading, authorization, rate limiting, remote stubRequest mapping, response mapping, unit conversion, method renamingName the wrapper by what boundary it manages
LLD exampleToppings around Beverage or enrichers around LoggerCached repository or protected account servicePayment provider SDK behind PaymentGatewayUse the simplest wrapper that matches the requirement
RiskToo many nested wrappers can hide order-sensitive behaviorProxy can become a policy dumping groundAdapter can leak vendor concepts into the domainKeep wrappers small and keep domain rules explicit

Bridge vs Adapter

DimensionBridgeAdapterDecision clue
When appliedDesigned up front when two dimensions vary independentlyAdded when an existing interface does not match the desired interfaceBridge prevents class explosion; Adapter repairs incompatibility
Main structureAbstraction holds an implementation interfaceAdapter implements the target interface and delegates to adapteeBridge splits hierarchy; Adapter wraps a mismatched collaborator
Change supportedNew abstractions and implementations can be combined independentlyNew adaptees can be supported by writing new adaptersUse Bridge for planned variation and Adapter for external boundaries
LLD exampleAlert type varies independently from delivery channelBank SDK is adapted to PaymentGatewayIf both axes are yours, consider Bridge; if one side is foreign, use Adapter

Where each structural pattern appears in LLD

PatternDesign pressureCommon problem
AdapterThird-party or legacy interface mismatchPayment gateway, notification system, shopping cart
DecoratorOptional behavior combinationsCoffee machine, logging framework, notification system
FacadeComplex subsystem orchestrationHotel booking, movie booking, food delivery
CompositeTree of parts and groupsFile system, text editor, trello
ProxyControlled access to real objectRate limiter, authentication service, API gateway
BridgeTwo independent variation axesNotification system, payment gateway, report exporter
FlyweightLarge count of similar objectsText editor, chess, inventory management

Best Practices

  • Start from the coupling or extensibility problem, then name the pattern.

  • Keep wrappers thin. Decorators, proxies, and adapters should not become hidden god objects.

  • Use interfaces when a wrapper must be substitutable for the wrapped object.

  • Prefer composition over inheritance for Decorator, Proxy, Bridge, and Facade collaborations.

  • Keep Adapter mapping at the system boundary so vendor request and response shapes do not leak inward.

  • Use Facade to simplify callers, not to bypass domain services or hide all errors.

  • For Composite, make recursive operations explicit and decide how cycles, ordering, and permissions are handled.

  • For Flyweight, clearly separate intrinsic shared state from extrinsic per-instance state.

Common Mistakes

  • ×

    Calling any wrapper an Adapter even when the interface does not change.

  • ×

    Using Decorator for mutually exclusive choices that should be Strategy or State.

  • ×

    Letting a Facade contain all business logic instead of delegating to cohesive services.

  • ×

    Building Composite without a clear leaf and group contract, forcing callers back into type checks.

  • ×

    Using Proxy to silently change business semantics instead of controlling access or lifecycle.

  • ×

    Using Bridge when a simple interface injection would be enough for one variation axis.

  • ×

    Applying Flyweight to mutable shared state, causing one object update to corrupt many logical instances.

  • ×

    Overexplaining pattern names while underexplaining the domain responsibility of each class.

Quiz

0/8 answered

  1. 1.Which pattern should you choose when a vendor SDK has methods that do not match your internal PaymentGateway interface?

  2. 2.Which pattern best fits optional behavior such as adding metrics and retry around the same HTTP client contract?

  3. 3.What is the main benefit of Facade in an LLD design?

  4. 4.A folder and a file should both support sizeInBytes, while a folder contains more nodes. Which pattern fits?

  5. 5.Which pattern should control lazy loading, caching, or authorization while keeping the same interface as the real object?

  6. 6.Bridge is most useful when:

  7. 7.What must be true for Flyweight to be safe?

  8. 8.Which statement best distinguishes Adapter from Bridge?

Flashcards

Cheat Sheet

Adapter: Convert a legacy, vendor, or incompatible interface into the domain contract. Use at boundaries.

Decorator: Add optional behavior around the same interface. Use for stackable features such as retry, metrics, masking, toppings, and extra channels.

Facade: Hide multi-service orchestration behind one simple API. Use for checkout, booking, upload, or onboarding flows.

Composite: Represent leaves and groups uniformly. Use for file systems, menus, bundles, comments, tasks, and organization trees.

Proxy: Preserve the same interface while controlling access to a real object. Use for cache, lazy load, auth, rate limiting, and remote stubs.

Bridge: Split abstraction from implementation when two axes vary independently. Use for message type plus channel, shape plus renderer, or payment flow plus provider.

Flyweight: Share immutable intrinsic state and keep per-instance extrinsic state outside. Use when object count is very high.

Interview rule: Explain the design pressure first, then the pattern. If the pattern does not reduce coupling, simplify the design.

References