Dynamic Proxy — Interview Questions
Reviewed by Gurusankar M. · Updated Aug 15, 2026
Short Answer
A dynamic proxy is a class generated at runtime that implements one or more interfaces and routes every method call to a single InvocationHandler.invoke(). That handler can add behaviour — logging, timing, transactions, security — before/after delegating to the real object. JDK proxies work on interfaces; CGLIB/ByteBuddy subclass concrete classes. It's the mechanism behind Spring AOP, @Transactional, and mock libraries.
On this page
Concept Made Simple
Coffee Chat Question
“How do Java dynamic proxies work, and how do frameworks use them for AOP?”
Remember It Faster
Mind Map Answer
A proxy is a stand-in that looks like the real interface but funnels every call through your invoke() method, so you can wrap cross-cutting behaviour around the real target without editing it.
- JDK proxy
- Proxy.newProxyInstance — interfaces only
- Handler
- InvocationHandler.invoke(proxy, method, args)
- CGLIB/ByteBuddy
- Subclass concrete classes (no interface)
- Powers
- Spring AOP, @Transactional, mocks
Spring wraps your bean in a proxy: calling save() actually hits the proxy, which opens a transaction, calls the real save(), then commits or rolls back — all without a line of transaction code in your method.
Key takeaway: proxies add behaviour around method calls at runtime. JDK = interfaces via InvocationHandler; CGLIB = subclassing — and self-invocation inside a bean bypasses the proxy.
Why CGLIB can't proxy a final class or method: CGLIB's whole mechanism is generating a runtime subclass of your target class that overrides each method to route through the interceptor. A final class can't be subclassed at all — there's nothing to generate. A final method on a non-final class can't be overridden either, so even a successfully generated subclass leaves that one method un-intercepted; a call to it goes straight to the real implementation and skips the proxy's advice entirely, silently.
If InvocationHandler.invoke() throws, the exception propagates to the caller exactly as if the real method had thrown it directly — the proxy transparently rethrows whatever invoke() throws (typically after Method.invoke() unwraps an InvocationTargetException back to its cause). The one sharp edge: if invoke() throws a checked exception that isn't declared in the proxied interface method's throws clause, the JVM wraps it in an unchecked UndeclaredThrowableException instead of letting it through as-is — a reminder that the proxy is bound by the interface's declared exception contract, not the real implementation's.
Multiple advices on one method compose as nested proxies, and order matters. A bean with both @Transactional and @Cacheable isn't wrapped by one proxy doing two things — Spring builds a chain, one proxy per advice, each wrapping the next like layers of an onion; the outermost proxy's invoke() runs first, calls into the next proxy, and so on until the innermost call finally reaches the real object. If @Cacheable sits outside @Transactional in that chain, a cache hit skips the transaction (and the real method) entirely and returns the cached value straight away — completely different behavior than the reverse ordering, where the transaction always opens even on a cache hit. Spring resolves this default ordering by advice type, but @Order (or Ordered) on the underlying Advisor lets you make it explicit rather than relying on the framework's default and being surprised later.
Proxies aren't free, but the overhead is smaller than it sounds. Every proxied call pays for one extra layer of indirection (dispatch through invoke()) plus, for JDK proxies specifically, a reflective Method.invoke() to reach the real implementation — reflection carries a real per-call cost relative to a direct method call, though the JIT compiler inlines and optimizes hot reflective call sites aggressively in long-running JVMs, so the gap narrows considerably after warm-up. In practice this overhead is negligible next to what the advice itself typically does (a database transaction, a cache lookup, a network call) — it becomes a real concern only in extremely hot, low-latency inner loops, which is exactly the kind of code most teams are careful not to route through Spring-managed beans in the first place.
Learn by Doing
Hands-on Keyboard
interface Repo { void save(String s); }
Repo real = s -> System.out.println("saved " + s);
Repo proxy = (Repo) Proxy.newProxyInstance(
Repo.class.getClassLoader(),
new Class<?>[]{ Repo.class },
(p, method, args) -> { // InvocationHandler
System.out.println("BEGIN tx");
Object r = method.invoke(real, args); // delegate
System.out.println("COMMIT tx");
return r;
});
proxy.save("order-1");BEGIN tx saved order-1 COMMIT tx
Think Beyond the Expected
What If?
Why does calling one @Transactional method from another method in the same bean skip the transaction?
Because the proxy only intercepts calls that come through it from outside. An internal `this.otherMethod()` call goes straight to the target object, bypassing the proxy wrapper entirely — so the advice (transaction, cache, retry) never runs. This 'self-invocation' gotcha is why Spring's proxy-based AOP can silently skip @Transactional/@Cacheable on internal calls; you fix it by refactoring into a separate bean, self-injecting the proxy, or switching to AspectJ weaving.
Real World
Spring's @Transactional, @Cacheable, @Async, @Retryable, and security checks are all proxy-based advice; Mockito generates proxies for mocks; mapping and RPC libraries proxy interfaces to turn method calls into network requests. The self-invocation gotcha is one of the most common 'why didn't my transaction roll back?' bugs in real Spring apps.
Interviewer's Expectation
Keywords they're listening for:
- runtime-generated proxy over interfaces
- InvocationHandler.invoke intercepts calls
- JDK (interface) vs CGLIB (subclass)
- powers Spring AOP/@Transactional
- self-invocation bypasses the proxy
Common Mistakes
- Expecting @Transactional to apply on self-invocation
- Trying to JDK-proxy a class with no interface
- Forgetting CGLIB can't subclass final classes/methods
Best Practices
- Program to interfaces so JDK proxies apply cleanly
- Avoid self-invocation for proxied cross-cutting concerns
- Use AspectJ weaving when proxy limits get in the way
Follow-up Questions
- JDK dynamic proxy vs CGLIB — when is each used?
- How does reflection enable dynamic proxies?
- Why can't CGLIB proxy a final class or method?
- How does the JDK dynamic proxy relate to the classic Proxy design pattern (GoF)?
- What happens if InvocationHandler.invoke() throws an exception?
References
Continue Learning with AI
Take this question deeper with your favourite AI assistant. Pick a depth, copy the prompt, or open it directly — AI is your learning companion, not a shortcut.
Plain-language foundations
I'm preparing for a software engineering interview and want to understand this from scratch, as a beginner. Topic: Dynamic Proxy (Advanced Java) Interview question: "How do Java dynamic proxies work, and how do frameworks use them for AOP?" Please: 1. Explain the core idea in simple, plain language, using an everyday analogy. 2. Define any technical terms you use. 3. Walk through one small, concrete example. 4. Finish with a single sentence I can easily remember. Keep the tone friendly and assume I'm new to this topic.
Was this answer helpful?
Affiliate links
Tools for Your Developer Workspace
Equipment for a comfortable, productive setup.
As an Amazon Associate I earn from qualifying purchases.
- MonitorView on Amazon : BenQ EW2790U 27-inch Monitor (opens in a new tab)
BenQ EW2790U 27-inch Monitor
- LaptopView on Amazon : Acer Aspire One Laptop (opens in a new tab)
Acer Aspire One Laptop
- Office ChairView on Amazon : Wakefit Gravita Ergonomic Office Chair (opens in a new tab)
Wakefit Gravita Ergonomic Office Chair