Facade — simplifying complex subsystems, Facade vs Adapter, a Java implementation (checkout orchestration), benefits in large systems and when it's a bad idea — and Proxy — controlling access, virtual/remote/protection proxies, JDK dynamic proxies and CGLIB, where Spring and Hibernate use proxies, and the downsides.
Published September 25, 2026
These are the two patterns you use daily without writing them: every Spring @Transactional bean is a proxy, and every well-designed service layer is a facade over repositories and clients. Ground your answers in those, including the classic self-invocation proxy trap.
Short answer: A facade provides one simple, higher-level interface to a complex subsystem made of many classes. Clients call placeOrder(), and the facade coordinates inventory, pricing, payment and shipping. The subsystem stays available for advanced users, and the facade just offers the common path.
Learn it in depth → Adapter & Facade
Short answer: A proxy is a stand-in object with the same interface as the real object, which controls access to it:
The client usually can't tell it isn't talking to the real object.
Learn it in depth → Composite & Proxy
Short answer:
Short answer: Facade defines a new, simpler interface over many classes, to reduce complexity. Adapter makes one existing interface conform to another expected interface, for compatibility. A facade is about convenience and orchestration, and an adapter is about translation. A facade can use adapters internally.
Short answer: Write a class that depends on the subsystem components, and exposes intention-revealing methods, coordinating them. In Spring, this is typically an application service.
@Service
public class CheckoutFacade {
private final InventoryService inventory; private final PricingService pricing;
private final PaymentGateway payments; private final ShippingService shipping; private final OrderRepository orders;
public CheckoutFacade(InventoryService i, PricingService p, PaymentGateway pay, ShippingService s, OrderRepository o) {
this.inventory = i; this.pricing = p; this.payments = pay; this.shipping = s; this.orders = o;
}
@Transactional
public OrderConfirmation checkout(Cart cart, PaymentDetails payment) {
inventory.reserve(cart.items());
Money total = pricing.totalFor(cart);
ChargeResult charge = payments.charge(total, payment.token());
if (!charge.approved()) { inventory.release(cart.items()); throw new PaymentDeclinedException(charge.reason()); }
Order order = orders.save(Order.placed(cart, total, charge.reference()));
shipping.schedule(order);
return OrderConfirmation.of(order);
}
}
Key points to cover:
watchMovie() calling the player, projector and amplifier) is the textbook version of the same idea.Short answer:
Key points to cover:
Short answer:
Key points to cover:
Short answer: The proxy implements the same interface as the real subject, and holds (or knows how to obtain) a reference to it. Every call goes through the proxy first, and the proxy decides whether, when and how to forward it: after checking permissions, after lazily creating the subject, over the network, from a cache, or within a transaction.
Short answer:
@PreAuthorize).Others: caching proxies (@Cacheable), smart references (reference counting, logging), and transactional proxies (@Transactional).
Short answer:
java.lang.reflect.Proxy) for interfaces;final classes and methods can't be proxied that way.@SuppressWarnings("unchecked")
static <T> T timed(T target, Class<T> iface, MeterRegistry meters) {
return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[]{iface}, (proxy, method, args) -> {
long start = System.nanoTime();
try { return method.invoke(target, args); }
catch (InvocationTargetException e) { throw e.getCause(); } // rethrow the real exception
finally { meters.timer("calls", "method", method.getName()).record(System.nanoTime() - start, TimeUnit.NANOSECONDS); }
});
}
Short answer: You already do, through frameworks:
@Async and @Retryable are all applied through proxies around beans.getReference().Write your own when you need to add access control, lazy initialisation, caching or instrumentation without touching the real class.
Common trap: self-invocation. Inside a Spring bean, this.otherMethod() bypasses the proxy, so @Transactional/@Cacheable on otherMethod don't apply. Call through another bean, or restructure.
Learn it in depth → Spring AOP
Short answer:
getClass() returns the proxy class, == compares against the proxy, and equals may trigger loading (Hibernate).final classes or methods. Self-invocation bypasses the proxy.LazyInitializationException outside a session, and hidden N+1 queries.Q: JDK dynamic proxy or CGLIB: which does Spring use?
A: Spring Boot defaults to CGLIB class-based proxies (proxyTargetClass=true), so beans can be injected by class type. JDK proxies are used when configured, and require injecting by interface.
Q: How do you call a proxied method from within the same bean?
A: Refactor the method into another bean (the cleanest option), inject the bean into itself lazily (@Lazy self-injection), or use AopContext.currentProxy() with exposeProxy. The last two are workarounds.
Q: Facade vs Mediator? A: A facade simplifies access for clients, and the subsystem doesn't know the facade exists. A mediator coordinates communication between peer objects, which talk to the mediator instead of to each other.
Q: How does Hibernate detect that an entity is a proxy?
A: With Hibernate.isInitialized(entity), or by checking instanceof HibernateProxy. Hibernate.unproxy(entity) returns the real object, initialising it if needed.