Proxy Pattern

Intermediate
Structural Patterns

Provides a surrogate or placeholder for another object to control access, add caching, logging, or lazy initialization.

Overview

A Proxy implements the same interface as the real subject and controls access to it. There are four main types: Virtual Proxy (lazy initialization — creates the expensive real object only on first use), Protection Proxy (access control — checks permissions before delegating), Remote Proxy (marshals calls across network boundaries — Java RMI), and Cache Proxy (memoizes results to avoid repeated expensive calls). Spring AOP uses JDK dynamic proxies (interface-based) or CGLIB proxies (subclass-based) to implement @Transactional, @Cacheable, and @Async transparently.

Virtual Proxy & Cache Proxy

A Virtual Proxy defers creation of an expensive object until it is first needed. A Cache Proxy stores results of expensive operations and returns cached results for identical requests.

Java — Cache Proxy and Protection Proxy
// Subject interface
public interface ImageLoader {
    byte[] loadImage(String imageId);
}

// Real Subject — expensive (hits S3)
public class S3ImageLoader implements ImageLoader {
    @Override
    public byte[] loadImage(String imageId) {
        System.out.println("Fetching from S3: " + imageId); // slow network call
        return new byte[]{1, 2, 3}; // simulated image data
    }
}

// Cache Proxy — wraps real loader, caches results
public class CachedImageLoader implements ImageLoader {
    private final ImageLoader delegate;
    private final Map<String, byte[]> cache = new ConcurrentHashMap<>();

    public CachedImageLoader(ImageLoader delegate) {
        this.delegate = delegate;
    }

    @Override
    public byte[] loadImage(String imageId) {
        return cache.computeIfAbsent(imageId, id -> {
            System.out.println("Cache MISS for: " + id);
            return delegate.loadImage(id);
        });
    }
}

// Protection Proxy — checks permissions before delegating
public class SecureImageLoader implements ImageLoader {
    private final ImageLoader delegate;
    private final SecurityContext security;

    public SecureImageLoader(ImageLoader delegate, SecurityContext security) {
        this.delegate = delegate;
        this.security = security;
    }

    @Override
    public byte[] loadImage(String imageId) {
        if (!security.hasPermission("IMAGE_READ")) {
            throw new AccessDeniedException("No permission to read image: " + imageId);
        }
        return delegate.loadImage(imageId);
    }
}

// Stacking proxies (Protection → Cache → Real)
ImageLoader loader = new SecureImageLoader(
                       new CachedImageLoader(
                         new S3ImageLoader()), securityCtx);
loader.loadImage("course-thumbnail.jpg"); // checks permission, then cache, then S3

Spring AOP Dynamic Proxy

Spring creates JDK dynamic proxies automatically for beans annotated with @Transactional, @Cacheable, @Async. The proxy intercepts method calls, runs cross-cutting concerns (begin transaction, check cache), then delegates to the real bean.

Java — Spring AOP Proxy (@Cacheable, @Transactional)
// Spring creates a proxy around this bean transparently
@Service
public class CourseService {

    @Cacheable(value = "courses", key = "#courseId")
    public CourseDto getCourse(String courseId) {
        // Spring proxy: check cache → if miss → run this method → store in cache
        System.out.println("Loading course from DB: " + courseId);
        return courseRepository.findById(courseId).map(CourseDto::from).orElseThrow();
    }

    @Transactional  // Spring proxy: begin tx → run method → commit/rollback
    public void publishCourse(String courseId) {
        Course c = courseRepository.findById(courseId).orElseThrow();
        c.setStatus(CourseStatus.PUBLISHED);
        courseRepository.save(c);
        eventPublisher.publishEvent(new CoursePublishedEvent(courseId));
    }
}

// What Spring generates behind the scenes (simplified JDK proxy):
// CourseService proxy = (CourseService) Proxy.newProxyInstance(
//     CourseService.class.getClassLoader(),
//     new Class[]{CourseService.class},
//     (proxyObj, method, args) -> {
//         if (method.isAnnotationPresent(Cacheable.class)) {
//             // check cache, return cached value or call real method
//         }
//         return method.invoke(realCourseService, args);
//     });

// IMPORTANT: @Transactional only works on public methods called from OUTSIDE the bean.
// Self-invocation bypasses the proxy: this.publishCourse() inside the same bean
// does NOT start a new transaction!

Key Points to Remember

  • 1Four proxy types: Virtual (lazy init), Protection (access control), Remote (network), Cache (memoization).
  • 2Proxy and Decorator look identical in code — intent differs: Proxy controls access; Decorator adds behavior.
  • 3Spring @Transactional and @Cacheable use dynamic proxies — self-invocation bypasses them.
  • 4JDK dynamic proxies require an interface; CGLIB proxies subclass the target (no interface needed).
  • 5Proxy is transparent to the client — client cannot tell it is talking to a proxy.

Interview Questions

Sign in to ask Aria
1

What are the four types of Proxy pattern? Give an example of each.

MediumAmazon
2

Why does @Transactional not work when a method calls another method in the same class?

HardGoogle
3

What is the difference between JDK dynamic proxy and CGLIB proxy in Spring?

HardNetflix
4

What is the difference between Proxy and Decorator patterns?

MediumMicrosoft
5

How would you implement a retry proxy for a flaky external API call?

MediumUber

Ask Aria about Proxy Pattern

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…