Understanding Proxy Patterns: The Why and How of Static and Dynamic Proxies in Java

Published: (December 29, 2025 at 05:49 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

Introduction

In the previous post we talked about Spring’s @Transactional annotation and saw how it does its magic with Spring AOP, thanks to the unsung hero working behind the scenes: dynamic proxies.

That got me thinking: why stop there? Proxies are used all the time in Spring, so why not do a deeper dive under the hood to understand how they work?

So today I’m excited to kick off a series of posts dedicated to unlocking the power of dynamic proxies! Think of them as your secret weapon for writing cleaner code. You can package up all that repetitive boilerplate once, and then use simple annotations to sprinkle the functionality anywhere you need it. It’s like writing a super‑power and then handing it out to your entire codebase.

We’ll start our journey with the classic Proxy pattern from the Gang of Four’s famous book, Design Patterns. We’ll connect the dots between this pattern and the dynamic proxies that frameworks like Spring use every day. To make sure we’re all on the same page, we’ll even build a simple static proxy together first.

And because the best way to learn is by doing, we’ll finish with a capstone project where we build our own annotation: @MyTransactional, mimicking the functionality of Spring’s @Transactional.

So, whether you’re completely new to proxies or you’re looking to get handy with advanced tools like ByteBuddy, pull up a chair! I hope this series will be a friendly and practical guide for you, and that you’ll have a better understanding of dynamic proxies at the end.

Let’s start with the Proxy Pattern

A proxy is used when you want to add a layer of control between the client calling a method and the actual object (the Real Subject) executing it. At its core, the Proxy Pattern provides an object that represents another object.

If that sounds a bit abstract, no worries—let’s break it down with an example.

  1. Client wants to use a service, which we’ll call the Subject.
  2. The Subject is an interface. The real work is performed by a class called the Real Subject, which implements that interface.
  3. The Proxy steps in between the Client and the Real Subject. When the Client calls a method on the Subject, the Proxy intercepts that call before it reaches the Real Subject. This gives the Proxy a chance to do extra work either before passing the request along or after getting the result back.

What can a proxy do?

Use‑caseExample
Access Control (Playing Bouncer)“Hold on, do you have the right permissions to make this call?”
Lazy Initialization (Being Lazy)“I won’t create this heavy object (e.g., a huge file or DB connection) until I absolutely have to.”
Remote Invocation (Being a Messenger)“The real object lives on another machine? I’ll handle the network communication for you.”
Cross‑Cutting Concerns (Handling the Annoying Stuff)“I’ll automatically take care of logging, caching, or starting a transaction so the main object stays clean.”

The beauty of all this? Your Real Subject can stay focused purely on business logic, while the proxy handles repetitive but important tasks. It’s like having a dedicated assistant that takes care of all the prep work and clean‑up!

Building a Static Proxy

Alright, we’ve learned what a Proxy is. Now let’s roll up our sleeves and build one together in Java!

1️⃣ Define the Subject (the contract)

interface Subject {
    void execute();
}

2️⃣ Implement the RealSubject (the real work)

public class RealSubject implements Subject {

    @Override
    public void execute() {
        System.out.println("Performing an expensive operation.");
        // ... an operation
    }
}

3️⃣ Create the Proxy (the middleman)

public class SubjectProxy implements Subject {

    private final RealSubject realSubject = new RealSubject();

    @Override
    public void execute() {
        System.out.println("Proxy intercepting RealSubject's operation.");
        // logging the method call
        // ... any extra work
        realSubject.execute();   // delegate to the real subject
    }
}

4️⃣ Use the proxy from the Client code

public class Client {

    private final Subject subject = new SubjectProxy();

    public void call() {
        subject.execute();
    }
}

As you can see, the proxy can seamlessly add its own functionality before or after the real method is called. In just a few lines you’ve created a helper that can manage, secure, or monitor access without touching the real object!

The Problem with Doing It Manually

Our SubjectProxy works great for a simple example, but imagine you have dozens of services, each with dozens of methods. Writing a static proxy for every interface quickly becomes:

  • Tedious – a lot of boilerplate code.
  • Error‑prone – easy to forget to delegate a method or to keep the proxy in sync with the interface.
  • Hard to maintain – any change to the interface forces you to update every proxy.

That’s why frameworks like Spring generate dynamic proxies at runtime. They let you write the cross‑cutting concern once (e.g., logging, transaction management) and automatically apply it to any bean that matches a pointcut, without manually writing a proxy class for each interface.

The N+1 Class Problem

Imagine you’re working on a massive enterprise application with hundreds of services. If you wanted to add logging or transaction management to every single one of them using a static approach, you’d have to write a separate proxy class for every service interface.

That’s a lot of boilerplate! It’s tedious, error‑prone, and—let’s be honest—not very “engineer‑y.” This is what we call the N+1 Class Problem: for every business class you write, you’re forced to write a corresponding proxy class.

There has to be a better way, right?

Enter the Dynamic Proxy: The Automated Middleman

If static proxies are like hand‑writing a custom contract for every single person you meet, Dynamic Proxies are like having a smart template that writes itself the moment it’s needed.

The core idea is simple: instead of writing SubjectProxy.java, we tell the Java Virtual Machine (JVM) at runtime:

“Hey, I need an object that looks like this interface, but whenever someone calls a method on it, send that call to this single Handler class I’ve written.”

Quick Example

// Our single "Handler" that handles EVERY method call for EVERY interface
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("Dynamic Proxy intercepting: " + method.getName());
    return method.invoke(realSubject, args);
};

// The Magic: Creating the proxy instance on the fly
Subject dynamicProxy = (Subject) Proxy.newProxyInstance(
    Subject.class.getClassLoader(),
    new Class[] { Subject.class },
    handler
);

dynamicProxy.execute(); // This call is intercepted by our handler!

Note: Don’t worry if this code looks unfamiliar. We’ll explore JDK dynamic proxies in more depth later in the series. The key takeaway is that we didn’t write a class called SubjectProxy; we generated it while the program was running. If we had 100 different interfaces, the same logic would handle all of them—no more N+1 problem.

Summary & What’s Next

We’ve moved from the classic design pattern to the reality of “manual labor” with static proxies, and we’ve glimpsed how Java can generate these middlemen dynamically to save us from drowning in boilerplate.

But is this just a neat trick for lazy developers? Far from it.

In the next post we’ll step out of our “Hello World” examples and look at Real‑World Magic. We’ll deep‑dive into how the giants of the Java ecosystem—Spring, Hibernate, and Mockito—use dynamic proxies to power the features we use every day. Specifically, we’ll examine:

  • How the @Transactional annotation works under the hood using proxies.
  • How Hibernate manages lazy loading of data.
  • How Mockito can simulate a method call and return mocked data.

Stay tuned for Part 2—it’s going to get interesting!

Back to Blog

Related posts

Read more »

Introduction to Java

How does Java code written by humans get executed by machines? It uses a translation system JDK → JRE → JVM → JIT to convert the programming language to machin...

2026: The Year of Java in the Terminal

Article URL: https://xam.dk/blog/lets-make-2026-the-year-of-java-in-the-terminal/ Comments URL: https://news.ycombinator.com/item?id=46445229 Points: 39 Comment...