Fundamentals 16 min read

Master the Four Structural Design Patterns: Proxy, Decorator, Adapter, Facade with Spring Code Examples

This article explains the four essential structural design patterns—Proxy, Decorator, Adapter, and Facade—detailing their core concepts, real‑world Java/Spring use cases, code implementations, and interview‑focused comparisons to help developers master enterprise‑level architecture.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Master the Four Structural Design Patterns: Proxy, Decorator, Adapter, Facade with Spring Code Examples

Why Structural Patterns Matter

Unlike Creational patterns (how objects are created) and Behavioral patterns (how objects interact), Structural patterns answer the question of how to compose objects like building blocks to form more powerful structures. The GoF catalog lists seven, but in enterprise development and interviews four dominate: Proxy, Decorator, Adapter, and Facade.

1. Proxy Pattern – The Soul of Spring AOP and RPC

Core Idea

Provide a proxy that controls access to another object, adding functionality without modifying the original code.

Anti‑example: Hard‑coded logging

public class UserServiceImpl implements UserService {
    public User getUserById(Long id) {
        long start = System.currentTimeMillis(); // 💩 non‑business logic intrusion
        log.info("开始查询用户: {}", id);
        User user = userDao.selectById(id); // real business
        log.info("查询结束,耗时: {} ms", System.currentTimeMillis() - start);
        return user;
    }
}

If a service has 100 methods, copying this logging code 100 times is impractical.

Static Proxy – Let the "stand‑in" handle the dirty work

// ① Real object – pure business logic
public class UserServiceImpl implements UserService {
    public User getUserById(Long id) {
        return userDao.selectById(id);
    }
}

// ② Proxy object – adds all enhancement logic
public class UserServiceProxy implements UserService {
    private UserService target; // holds reference to real object
    public UserServiceProxy(UserService target) { this.target = target; }
    @Override
    public User getUserById(Long id) {
        long start = System.currentTimeMillis();
        log.info("[Proxy] 开始查询用户: {}", id);
        User user = target.getUserById(id);
        log.info("[Proxy] 查询结束,耗时: {} ms", System.currentTimeMillis() - start);
        return user;
    }
}

Structure: proxy and real class implement the same interface; the proxy holds a reference to the real object and executes enhancement logic before delegating.

Static Proxy Pain Points

When UserService has 50 methods, the proxy must duplicate all 50 signatures.

Multiple services (e.g., OrderService, PayService) would require dozens of proxy classes.

Dynamic Proxy – The Foundation of Spring AOP and Dubbo

At runtime, reflection generates a proxy class, allowing a single interceptor to handle all methods.

JDK Dynamic Proxy : based on interfaces, uses Proxy.newProxyInstance() + InvocationHandler.

CGLIB Dynamic Proxy : based on subclassing, uses ASM bytecode generation; Spring falls back to it when the target class lacks an interface.

In practice developers annotate with @Aspect + @Around; the underlying mechanism is dynamic proxy.

Real‑world Mapping

Spring AOP – @Transactional, @Cacheable are implemented via dynamic proxies.

MyBatis – MapperProxy creates a proxy for mapper interfaces.

Dubbo RPC – the consumer invokes a remote service through a proxy, making network communication transparent.

2. Decorator Pattern – Java IO and Runtime Feature Extension

Core Idea

Dynamically add responsibilities to an object. Compared with subclassing, decorators are more flexible and avoid class explosion.

Anti‑example: Subclass explosion with milk‑tea toppings

Base milk tea costs 10 元; adding pearl (+2 元), coconut (+3 元), boba (+4 元). Using inheritance for every combination would require 2¹⁰ = 1024 classes.

Decorator Implementation

// ① Abstract component
public interface Drink {
    String getDescription();
    int cost();
}

// ② Concrete component – basic milk tea
public class BaseMilkTea implements Drink {
    public String getDescription() { return "基础奶茶"; }
    public int cost() { return 10; }
}

// ③ Abstract decorator – holds a Drink reference and implements Drink
public abstract class DrinkDecorator implements Drink {
    protected Drink drink;
    public DrinkDecorator(Drink drink) { this.drink = drink; }
}

// ④ Concrete decorator – add pearl
public class PearlDecorator extends DrinkDecorator {
    public PearlDecorator(Drink drink) { super(drink); }
    public String getDescription() { return drink.getDescription() + " + 珍珠"; }
    public int cost() { return drink.cost() + 2; }
}

// ⑤ Concrete decorator – add coconut
public class CoconutDecorator extends DrinkDecorator {
    public CoconutDecorator(Drink drink) { super(drink); }
    public String getDescription() { return drink.getDescription() + " + 椰果"; }
    public int cost() { return drink.cost() + 3; }
}

Client Usage – Stack decorators as needed

// Customer orders: basic milk tea + pearl + coconut
Drink myDrink = new CoconutDecorator(
    new PearlDecorator(
        new BaseMilkTea()
    )
);
System.out.println(myDrink.getDescription()); // 输出:基础奶茶 + 珍珠 + 椰果
System.out.println("总价: " + myDrink.cost() + " 元"); // 输出:总价: 15 元

Changing order or adding more layers is trivial, demonstrating the flexibility over inheritance.

Real‑world Mapping – Java IO Streams

Java IO uses the decorator pattern: each stream wraps the previous one, adding capabilities such as buffering, character conversion, etc.

BufferedReader reader = new BufferedReader(
    new InputStreamReader(
        new FileInputStream("a.txt")
    )
);
InputStream / Reader

– abstract component. FileInputStream – concrete component (lowest level). FilterInputStream – abstract decorator. BufferedInputStream – concrete decorator adding buffering. DataInputStream – concrete decorator adding primitive‑type read/write.

3. Proxy vs. Decorator – Interview‑Level Distinction

Proxy focuses on "control"; Decorator focuses on "enhancement".

Core purpose : Proxy – control access, hide implementation, intercept methods; Decorator – dynamically add behavior, stack features.

Where the wrapper comes from : Proxy – created/injected inside the proxy class, invisible to the caller; Decorator – supplied by the client via constructor, caller actively composes.

Binding time : Proxy – determined at compile‑time or startup (strong binding); Decorator – composed at runtime (highly flexible).

Typical use cases : Proxy – Spring AOP, MyBatis mapper, Dubbo RPC; Decorator – Java IO streams, Spring transaction/cache wrappers.

4. Adapter Pattern – The "Translator" that Bridges Incompatible Interfaces

Core Idea

Convert one interface into another expected by the client, enabling incompatible components to work together.

Business Scenario: Dual payment callbacks (Alipay & WeChat)

Alipay callback: trade_no, total_amount
WeChat callback: transaction_id, total_fee
Internal system expects: orderId, amount

Each third‑party uses its own “dialect”; the internal system speaks a single “language”.

Solution – Write an adapter for each third‑party

// Standard internal interface
public interface PayCallback {
    String getOrderId();
    int getAmount();
}

// Alipay adapter – translates Alipay fields to the standard
public class AliPayAdapter implements PayCallback {
    private AliPayCallbackDTO aliPayData;
    public AliPayAdapter(AliPayCallbackDTO data) { this.aliPayData = data; }
    @Override public String getOrderId() { return aliPayData.getTrade_no(); }
    @Override public int getAmount() { return (int)(aliPayData.getTotal_amount() * 100); }
}

// WeChat adapter – translates WeChat fields to the standard
public class WechatPayAdapter implements PayCallback {
    private WechatCallbackDTO wechatData;
    public WechatPayAdapter(WechatCallbackDTO data) { this.wechatData = data; }
    @Override public String getOrderId() { return wechatData.getTransaction_id(); }
    @Override public int getAmount() { return wechatData.getTotal_fee(); }
}

Business logic depends only on PayCallback. Adding a new channel only requires a new adapter, leaving core code untouched.

Real‑world Mapping

Java IO – InputStreamReader adapts InputStream to Reader.

Spring MVC – HandlerAdapter adapts various handlers to a unified processing flow.

SLF4J – a logging façade that adapts Log4j, Logback, etc., to a common API.

5. Facade Pattern – The "Big Butler" for Microservice‑Era Complexity

Core Idea

Provide a unified high‑level interface to a complex subsystem, hiding internal intricacies and reducing client coupling.

Business Scenario: One‑click order in an app

Front‑end would otherwise need to call four microservices (inventory, coupon, order, points), causing network overhead and tight coupling.

// Facade class – the “big butler” for one‑click ordering
@Service
public class OrderFacade {
    @Autowired private InventoryService inventoryService;
    @Autowired private CouponService couponService;
    @Autowired private OrderService orderService;
    @Autowired private PointsService pointsService;

    public OrderResult createOrder(CreateOrderRequest request) {
        // ① lock stock
        inventoryService.lockStock(request.getSkuId(), request.getQuantity());
        // ② redeem coupon
        couponService.redeem(request.getCouponId(), request.getUserId());
        // ③ create order
        Order order = orderService.create(request);
        // ④ grant points
        pointsService.grant(request.getUserId(), order.getAmount());
        return OrderResult.success(order.getId());
    }
}

Front‑end now calls a single endpoint POST /order/create; the facade hides all internal service orchestration.

Real‑world Mapping

API Gateway (Nginx/Kong) – a façade exposing a unified entry point.

Spring Boot auto‑configuration – a single annotation ( @SpringBootApplication) that wires dozens of configuration classes.

SLF4J Logger – a façade that presents a simple log.info() API while hiding the underlying logging framework.

6. Quick‑Reference Card for the Four Structural “Kings”

Proxy – “Find a stand‑in to do the work”; solves adding logging, permissions, transactions, RPC without changing original code; typical in Spring AOP, MyBatis, Dubbo.

Decorator – “Stack toys to add ingredients”; avoids subclass explosion, enables dynamic feature stacking; typical in Java IO streams, caching/limiting wrappers.

Adapter – “Interface mismatch? I’ll bridge it”; reconciles incompatible APIs, useful for payment gateway integration, legacy system refactoring.

Facade – “Unified entry for easy management”; hides complex subsystems, ideal for microservice API gateways, BFF aggregation layers.

Key Takeaways

Proxy and Decorator look similar in code structure but differ in intent: one controls, the other enhances.

Adapter and Facade are both “middle‑layer” concepts – Adapter smooths differences, Facade shields complexity.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

design patternsjavaProxySpringAdapterDecoratorFacade
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.