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

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 just once, and then use simple annotations to sprinkle the functionality anywhere you need it. It’s like writing a superpower 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 do a capstone project at the end, where we will 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 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.

Imagine you have a Client who wants to use a service, which we’ll call the Subject. Normally, the Client could just talk directly to the Subject to get what it needs.

Now, let’s say our Subject is actually an interface. The real work is performed by a class called the Real Subject, which implements that interface.

This is where our Proxy comes in! It 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 some extra work either before passing the request along or after getting the result back.

So, what kind of “extra work” can this proxy do? Lots of useful things!

  • Playing Bouncer (Access Control): “Hold on, do you have the right permissions to make this call?”
  • Being Lazy (Lazy Initialization): “I won’t create this heavy object (like a huge file or database connection) until I absolutely have to.”
  • Being a Messenger (Remote Invocation): “The real object is actually on another machine? No problem, I’ll handle the long-distance communication for you.”
  • Handling the Annoying Stuff (Cross-Cutting Concerns): “I’ll automatically take care of logging, caching, or starting a transaction so the main object doesn’t have to.”

The beauty of all this? Your Real Subject can stay clean and focused purely on the business logic. All the other important but repetitive tasks are handled by the proxy. 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!

First up, we need to define our Subject. Think of this as the contract for a service our client wants to use.

interface Subject {
    void execute();
}

This simple interface has just one method: execute(). Next, let’s create the real deal, our RealSubject, which does the actual heavy lifting:

public class RealSubject implements Subject {

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

And now for the star of the show: the Proxy itself! It also implements the Subject interface, acting as a helpful middleman.

public class SubjectProxy implements Subject {
    private RealSubject realSubject = new RealSubject();

    @Override
    public void execute() {
        System.out.println("Proxy intercepting Real Subject's operation")
        // logging the method call
        // passing control to real subject  
        realSubject.execute();
    }
}

Finally, our client code simply interacts with the proxy, blissfully unaware of the extra steps happening behind the scenes.

public class Client {
    private Subject subject = new SubjectProxy();
    public void call() {
        subject.execute();
    }
}

As you can see, the beauty of the proxy is its ability to seamlessly add its own functionality before or after the real method is called. And just like that, you’ve created a clever helper that can manage, secure, or monitor access without changing the real object!

The Problem with Doing it Manually

Our SubjectProxy works great for a simple example. But 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 this “static” approach, you’d have to write a separate proxy class for every single 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 us 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.”

To give you a little teaser, here is how you create a dynamic proxy in just a few lines of code using the built-in JDK tools:

// 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!

Don’t worry if this code seems unfamiliar to you. We’ll get to know more about JDK dynamic proxies further along in the series. But do notice their power here: we didn’t write a class called SubjectProxy. We generated it while the program was running. If we had 100 different interfaces, we could use this same logic to handle all of them. No more N+1 problem.

Summary & What’s Next

We’ve traveled from the classic design pattern to the reality of “manual labor” with static proxies. We’ve also seen a glimpse of how Java allows us to 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’re going to step out of our “Hello World” examples and look at Real-World Magic. We will do a deep dive into how the giants of the Java ecosystem: Spring, Hibernate, and Mockito, use these dynamic proxies to power the features we use every day. We’ll look at how the @Transactional annotation works under the hood using proxies, how Hibernate manages to load data only when you ask for it, and how Mockito is able to simulate a method call and return mocked data.

Stay tuned for Part 2. It’s going to get interesting!

Leave a Reply

Your email address will not be published. Required fields are marked *