Proxy Pattern Interview Questions | JiQuest

add

#

Proxy Pattern

Java design pattern deep dive

Proxy Pattern in Java: 100 interview questions with professional answers.

Learn how the Proxy pattern controls access to an object through a stand-in that implements the exact same interface, when to reach for virtual, protection, remote, or caching proxies, how java.lang.reflect.Proxy and Spring AOP generate proxies at runtime, and how to avoid the bugs that make a proxy leak or stop being substitutable for the real thing.

100Scenarios
3Proxy types
4Related patterns
Clientexpects Subject Subjectinterface Proxyimplements Subject RealSubjectdoes the real work Proxy and RealSubject both implement Subject; the proxy controls access and forwards the call

What makes a good Proxy answer?

Interviewers want to see that you understand access control, not just wrapping syntax: the proxy must expose the identical interface as the real subject, add only the specific control it exists for, and stay substitutable everywhere the real subject is expected.

Same interface as RealSubjectUnlike an Adapter, a Proxy implements the exact same interface the client already calls; it never translates to a different contract.
Controlled access is the pointLazy creation, permission checks, network hops, or caching are the reason the proxy exists, not incidental extras bolted on.
Forwarding stays thinBeyond its one control concern, the proxy should delegate immediately; unrelated business logic belongs in the real subject.
Substitutable (Liskov)Client code must behave identically whether it holds the real subject or the proxy; nothing should notice the difference.
Need to controlaccess to an object? Expensive to create?Virtual proxy / lazy init Need access checks?Protection proxy Object is remote?Remote proxy (RMI/gRPC stub) Cross-cutting logic?Dynamic / AOP proxy
ApproachUse whenWatch out for
Virtual proxyConstructing the real subject is expensive (large image, JPA association, parsed template) and might never be needed.Thread-safety of the lazy-init check; a naive if-null test races under concurrent access.
Protection proxyDifferent callers should see different permission levels on the same object without duplicating checks in every caller.A proxy that fails open on an unexpected exception instead of denying by default.
Remote proxyThe real subject lives in another process or machine and callers should not hand-roll network code.Hiding partial failure and latency so well that callers forget the call can fail or block.
Caching proxyThe same call is repeated with the same arguments and the underlying data does not change on every read.Stale results after the source changes, and unbounded cache growth without eviction.
Dynamic proxy (JDK reflection or CGLIB)You need the same cross-cutting control (logging, transactions, retries) applied uniformly across many interfaces or classes.JDK proxies need an interface; CGLIB can't proxy final classes/methods, and self-invocation bypasses both.

Topics

Proxy basics Q1Same interface, not different Q2Four classic proxy types Q3 Static vs dynamic proxy Q4Hand-written proxy code Q5Proxy vs adapter Q6 Proxy vs decorator Q7Proxy vs facade Q8Wrapper family comparison Q9 Substitutability (LSP) Q10Virtual proxy basics Q11Lazy image loader Q12 Hibernate lazy association Q13N+1 query problem Q14LazyInitializationException Q15 Thread-safe lazy init Q16Lazy thumbnail gallery Q17Lazy report engine Q18 Proxy vs lazy getter Q19Async virtual proxy Q20Protection proxy basics Q21 Secured admin API Q22AccessController history Q23Feature-flagged proxy Q24 Protection proxy security risks Q25Proxy vs scattered checks Q26Logging denied access Q27 Multi-tenant isolation Q28Restricted file system proxy Q29Testing a protection proxy Q30 Remote proxy basics Q31RMI stub/skeleton Q32gRPC generated stub Q33 Hand-written HTTP proxy Q34Retries/timeouts in proxy Q35Marshalling concerns Q36 Latency & partial failure Q37Circuit breaker proxy Q38DB connection pooling proxy Q39 RMI vs service mesh Q40Caching proxy basics Q41Memoizing expensive calls Q42 Cache invalidation edge cases Q43Thread-safe cache proxy Q44Cache stampede Q45 Caching: proxy or decorator? Q46LRU eviction Q47Distributed cache proxy Q48 Cache key pitfalls Q49Spring @Cacheable Q50Logging/monitoring proxy Q51 Metrics-collecting proxy Q52Smart reference counting Q53Copy-on-write proxy Q54 Audit logging proxy Q55Rate-limiting proxy Q56Stacking multiple proxies Q57 Null object vs proxy Q58Retry-with-backoff proxy Q59A/B testing proxy Q60 JDK dynamic proxy internals Q61Generic caching InvocationHandler Q62Why JDK proxy needs an interface Q63 Runtime bytecode generation Q64equals/hashCode in proxies Q65JDK proxy performance Q66 Multiple interfaces, one proxy Q67UndeclaredThrowableException Q68Testing dynamic-proxy code Q69 Reusable proxy factory Q70CGLIB / ByteBuddy proxies Q71Final classes can't be proxied Q72 Spring's JDK vs CGLIB choice Q73@Transactional via proxy Q74Self-invocation bug Q75 Fixing self-invocation Q76Final methods silently skipped Q77ByteBuddy vs CGLIB vs JDK Q78 Proxy type mismatch bugs Q79AOP advice as interception Q80Leaking the real subject Q81 Breaking substitutability Q82equals/hashCode forgotten Q83Serializing a proxy Q84 Deep proxy stack traces Q85Reentrant proxy deadlock Q86Caching proxy memory leak Q87 Interface/impl version skew Q88CGLIB no-arg constructor Q89Protection proxy fails open Q90 Testing strategy overview Q91DB connection pool internals Q92ORM lazy collection N+1 fix Q93 Proxy + observer Q94Proxy + command Q95Service mesh sidecar Q96 "Isn't every wrapper a proxy?" Q97Benchmarking proxy overhead Q98Facade+proxy for migration Q99 When to reach for Proxy Q100

Interview questions and answers

Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.

1. Explain the Proxy design pattern in Java: describe the Subject, RealSubject, and Proxy roles, and the real-world problem it solves by controlling access to an object rather than translating its interface.

The Proxy pattern introduces a stand-in object, the Proxy, that implements the same Subject interface as the real object it represents, the RealSubject, and controls access to it: deciding whether, when, and how the real call actually happens. Client code depends only on the Subject interface, so it can hold either a RealSubject or a Proxy without knowing or caring which one it has.

The problem it solves is access control in the broad sense: creating an expensive object lazily, checking permissions before a call, hiding that an object lives on another machine, or caching results, all without changing a single line of the calling code or the RealSubject's own implementation.

interface AccountService {
    Balance getBalance(String accountId);
}

class RealAccountService implements AccountService {
    public Balance getBalance(String accountId) {
        // expensive or sensitive real work
        return new Balance(accountId, 1000);
    }
}

class AccountServiceProxy implements AccountService {
    private final RealAccountService real = new RealAccountService();

    @Override
    public Balance getBalance(String accountId) {
        // control point: logging, checks, caching, etc.
        return real.getBalance(accountId);
    }
}
Structural patternSame interface as real subjectAccess control

2. Explain precisely why a Proxy must implement the same interface as its RealSubject, whereas an Adapter deliberately implements a different interface than its Adaptee, and why confusing the two leads to a design mistake.

Proxy and Adapter are structurally similar, both hold a reference to another object and forward calls, but their intents are opposite. A Proxy exists so client code written against the Subject interface never has to change; it is a drop-in replacement for the RealSubject, so it must expose exactly the same methods with exactly the same signatures. An Adapter exists precisely because the client's expected interface and the wrapped object's interface are different, and its whole job is bridging that gap.

Confusing the two produces a proxy that quietly changes shape or behavior, breaking the substitutability callers rely on, or an adapter that pointlessly duplicates the adaptee's interface instead of translating it, missing the reason to adapt in the first place.

Proxy: same interfaceAdapter: different interface

3. List and describe the four classic proxy categories from the Gang of Four: virtual proxy, protection proxy, remote proxy, and smart reference, with one line summarizing each one's control point.

A virtual proxy defers creation of an expensive RealSubject until it is actually needed, standing in as a lightweight placeholder until then. A protection proxy checks the caller's permissions before allowing a call through to the real subject, denying or restricting calls the caller is not authorized to make. A remote proxy represents an object that lives in a different address space, typically another process or machine, hiding the network call behind what looks like an ordinary local method call. A smart reference proxy performs additional bookkeeping around access, such as counting references, enforcing a lock, or triggering a side effect, whenever the real object is accessed.

Modern Java code adds two more categories in practice, though both are variations on these four: caching proxies (a specialization of smart reference) and cross-cutting AOP proxies generated dynamically (a specialization used heavily by frameworks like Spring).

VirtualProtectionRemoteSmart reference

4. Compare static (hand-written or compile-time) proxies with dynamic (runtime-generated) proxies in Java, and explain the trade-offs between writing one proxy class per interface versus generating proxies at runtime.

A static proxy is an ordinary class, written by hand, that implements a specific interface and forwards to a specific RealSubject type. A dynamic proxy is generated at runtime, typically via java.lang.reflect.Proxy or a bytecode library like CGLIB, and dispatches every call through a single generic handler rather than through hand-written methods.

Static proxies are simple to read, debug, and step through, and the compiler catches signature mismatches immediately, but they do not scale well when the same cross-cutting behavior, such as logging or transactions, must apply identically across dozens of unrelated interfaces. Dynamic proxies eliminate that duplication at the cost of reflection overhead, weaker compile-time safety, and stack traces that are harder to read because every call passes through the same generic invocation path.

Static proxyDynamic proxyCompile-time vs runtime

5. Write a complete hand-written Proxy implementation in Java for a ReportGenerator interface, showing the Subject interface, the RealSubject class, and a Proxy class that adds a simple control point before delegating.

The proxy implements ReportGenerator exactly like the real class does, holds a reference to the real generator, and wraps the delegated call with whatever control point it exists to add, here a simple timing measurement, before returning the identical result type the client already expects.

interface ReportGenerator {
    Report generate(ReportRequest request);
}

class RealReportGenerator implements ReportGenerator {
    @Override
    public Report generate(ReportRequest request) {
        // expensive: queries a warehouse, renders a PDF, etc.
        return new Report(request.id(), "...rendered content...");
    }
}

class LoggingReportGeneratorProxy implements ReportGenerator {
    private final ReportGenerator real;

    LoggingReportGeneratorProxy(ReportGenerator real) { this.real = real; }

    @Override
    public Report generate(ReportRequest request) {
        long start = System.nanoTime();
        Report result = real.generate(request);
        long elapsedMs = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Report " + request.id() + " generated in " + elapsedMs + "ms");
        return result;
    }
}

6. Explain the difference between the Proxy pattern and the Adapter pattern with a concrete example showing when each is the correct choice.

Proxy keeps the client's existing interface unchanged and controls access to an object that already satisfies it; Adapter changes an incompatible interface into the one the client already expects. Both wrap another object and delegate, but the reason for wrapping is different: Proxy is about the same contract, guarded or optimized; Adapter is about a different contract, translated.

Use Proxy when a PaymentProcessor client already exists and you want a stand-in, such as a caching or access-checking wrapper, around that exact same PaymentProcessor interface. Use Adapter when the client expects PaymentProcessor but the object you have is a third-party class exposing a completely different, incompatible interface that needs its calls translated first.

7. Explain the difference between the Proxy pattern and the Decorator pattern, since both implement the same interface as the object they wrap, and clarify how their differing intents lead to different design decisions even though their code often looks nearly identical.

Structurally, Proxy and Decorator are nearly indistinguishable: both implement the same interface as the wrapped object and hold a reference to it. The difference is intent. A Decorator's job is to add new behavior or responsibility, and it is designed to be stacked, multiple decorators wrapping each other, each contributing independent behavior. A Proxy's job is to control access, and it is usually a single layer that decides whether, when, or how a call reaches the real subject, not to pile on unrelated features.

This shows up in construction too: a Decorator typically wraps whatever instance the caller hands it at the call site, chosen freely and stacked; a Proxy typically owns or creates its RealSubject itself, because part of its job, like lazy creation, is to control exactly when that real instance comes into existence.

// Decorator: adds behavior, meant to be stacked by the caller
Coffee order = new MilkDecorator(new SugarDecorator(new SimpleCoffee()));

// Proxy: controls access, usually owns/creates the real subject itself
Image thumbnail = new VirtualImageProxy("photo.jpg"); // decides *when* RealImage is built
Same interface (both)Adds behavior (Decorator)Controls access (Proxy)

8. Explain the difference between the Proxy pattern and the Facade pattern, given both sit in front of another object, and describe a scenario where each is the correct architectural choice.

A Proxy stands in for a single object behind the exact same interface that object already exposes; the client cannot tell it isn't talking to the real thing directly. A Facade introduces a brand-new, simplified interface in front of an entire subsystem of many classes, deliberately reducing the surface the client has to learn; it never pretends to be any one of the underlying classes.

Use Proxy when you have one ImageLoader interface and want to lazily load or cache behind it. Use Facade when you have a dozen classes for checkout, inventory, and shipping and want one CheckoutFacade.completeOrder() entry point that hides all of them.

9. Produce a single comparison summarizing Proxy, Adapter, Decorator, and Facade side by side: whether the interface changes, whether stacking is expected, and each pattern's core intent.

All four are "wrapper" patterns in the loose sense, but interviewers expect you to place them precisely by two axes: does the public interface change, and is the pattern designed to be composed in layers.

PatternInterface vs wrapped objectCore intent
ProxyIdentical interfaceControl access (lazy, protected, remote, cached)
AdapterDifferent interfaceTranslate an incompatible interface into an expected one
DecoratorIdentical interface, designed to stackAdd behavior/responsibility around existing calls
FacadeNew, simplified interface over many classesReduce subsystem complexity for callers

10. Explain how the Liskov Substitution Principle applies to the Proxy pattern, and describe a concrete bug that occurs when a proxy is not truly substitutable for its real subject.

Liskov's principle says client code written against a supertype must work correctly no matter which subtype it actually receives. Applied to Proxy, this means any code written against the Subject interface must behave the same whether it holds a RealSubject or a Proxy; the proxy must never surprise the caller with different exceptions, different nullability, or different side-effect ordering than the real subject would produce.

// Bug: caller special-cases on concrete type, breaking substitutability
if (service instanceof RealAccountService) {
    // works
} else {
    // silently takes a different, buggy path when given a proxy
}
Why this matters The whole value of Proxy collapses the moment callers need to know, via instanceof, casting, or divergent behavior, whether they are holding the real object or a stand-in.

11. Explain the virtual proxy category of the Proxy pattern in detail: what "virtual" means here, and why lazy initialization is the defining characteristic rather than an incidental optimization.

"Virtual" refers to the RealSubject not yet existing, virtually present through the proxy, until the moment it is genuinely needed. The proxy implements the full Subject interface immediately and cheaply, while the expensive construction, disk reads, network calls, or heavy parsing, is deferred until the first real method call actually requires the result.

This is the defining characteristic, not a bonus, because a virtual proxy that eagerly builds the real subject in its constructor is no longer solving the problem it exists for; the whole reason to introduce the proxy class instead of just constructing the real object directly is to control precisely when construction happens.

Lazy initializationDeferred construction

12. Implement a virtual proxy in Java for an expensive-to-construct ImageLoader class that only loads the actual image bytes from disk the first time render() is called.

The proxy stores only the lightweight path string at construction time and creates the real, expensive RealImage only inside render(), and only on the first call; every subsequent call reuses the already-loaded instance.

interface Image {
    void render();
}

class RealImage implements Image {
    private final String path;
    RealImage(String path) {
        this.path = path;
        loadFromDisk(); // expensive
    }
    private void loadFromDisk() { /* read bytes into memory */ }
    public void render() { System.out.println("Rendering " + path); }
}

class VirtualImageProxy implements Image {
    private final String path;
    private RealImage real;

    VirtualImageProxy(String path) { this.path = path; }

    @Override
    public void render() {
        if (real == null) {
            real = new RealImage(path); // created only when first needed
        }
        real.render();
    }
}

13. Explain how Hibernate and JPA implement lazy loading of entity associations using dynamically generated virtual proxy classes, and what the generated proxy actually looks like at runtime.

When an entity association is mapped as lazy, Hibernate does not return the real target entity from a getter; it returns a bytecode-generated subclass, produced at startup via ByteBuddy (or historically Javassist/CGLIB), that extends the entity class and overrides its accessor methods. That generated proxy holds a reference back to the persistence session and the entity's identifier but no other loaded state.

The first time any method other than the identifier getter is called on that proxy, it triggers hibernateLazyInitializer.initialize(), which issues the actual SELECT and populates the proxy's delegate target, after which every call forwards transparently to the now-loaded real entity.

HibernateBytecode-generated virtual proxy

14. Explain the N+1 query problem that arises from misusing Hibernate's lazy-loaded association proxies, with an example, and describe at least two ways to fix it.

N+1 happens when one query loads N parent entities, and then, because an association is lazy, accessing that association inside a loop triggers one additional query per parent, for a total of N+1 round trips instead of one or two.

List<Order> orders = orderRepository.findAll(); // 1 query
for (Order order : orders) {
    order.getLineItems().size(); // N additional queries, one lazy proxy initialization each
}

Fix it either by fetching eagerly up front for this specific query with JOIN FETCH, or by configuring @BatchSize so Hibernate initializes several proxies in one batched IN-clause query instead of one query per entity.

Watch out Switching the mapping to eager globally "fixes" this call site but silently makes every other query against the entity slower by always joining the association, even when it is never used.

15. What causes a LazyInitializationException when accessing a Hibernate lazy-proxy association, and how would you fix it properly rather than just switching the association to eager?

The proxy's initialize() logic needs an open persistence session to run its SELECT. If the entity is detached, typically because the transactional session that loaded it has already closed by the time a view layer or a serializer touches the lazy association, the proxy has no session to initialize itself with, and Hibernate throws LazyInitializationException instead of silently returning incomplete data.

@Transactional
public OrderDto getOrder(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    return OrderDto.from(order); // access lazy fields here, while the session is still open
}

The correct fix is to access every lazy association you need while still inside the transactional boundary, typically by mapping to a DTO before the method returns, rather than widening the transaction's scope or defaulting every mapping to eager.

16. Describe how to make the lazy-initialization check inside a virtual proxy thread-safe, and explain the race condition that a naive if (real == null) check has under concurrent access.

Without synchronization, two threads can both observe real == null, both construct their own RealImage, and one of the two writes gets lost or, worse, both partially-constructed writes race on the same field with no happens-before guarantee, so a second thread might observe a non-null but incompletely initialized reference.

class ThreadSafeVirtualProxy implements Image {
    private final String path;
    private volatile RealImage real;

    ThreadSafeVirtualProxy(String path) { this.path = path; }

    @Override
    public void render() {
        RealImage local = real;
        if (local == null) {
            synchronized (this) {
                local = real;
                if (local == null) {
                    real = local = new RealImage(path); // double-checked locking, volatile required
                }
            }
        }
        local.render();
    }
}

17. Design a virtual proxy for an image gallery application where each thumbnail's full-resolution image is loaded from disk or network only when the user actually opens it.

Each gallery cell holds a lightweight ThumbnailProxy, cheap to construct in bulk for hundreds of items, that implements the same Photo interface as the real full-resolution photo. Only when the user clicks to open one photo does that proxy load, decode, and cache the actual full-resolution bytes; every unopened photo in the gallery costs almost nothing in memory.

Bulk lightweight placeholdersLoad-on-demand

18. Design a virtual proxy for a reporting engine so that a large underlying dataset is only queried and loaded into memory the first time a report actually needs it, rather than eagerly for every report request.

A ReportDataSetProxy implements the same DataSet interface the report renderer expects, holding only the query parameters at construction. The first call to a data-access method, such as rows(), triggers the actual warehouse query and caches the result set on the proxy instance for the remainder of that report's rendering, so a report that only needs summary counts never pays for loading full row data.

19. Explain why formalizing lazy initialization as a Proxy class, implementing the same interface as the real object, is preferable to simply adding an if-null check and a lazy field directly inside a getter method on the class itself.

An inline lazy getter mixes two responsibilities into one class: doing the real work and deciding when to create it, so every consumer of that class is forced to depend on the concrete class even if all they need is the interface. A virtual proxy separates these cleanly: the real class stays a plain, eagerly-constructible implementation, and laziness becomes an orthogonal, swappable concern that can be added, removed, or tested independently, and that other code can depend on purely through the Subject interface.

Separation of concernsSwappable laziness

20. Explain how a virtual proxy can be combined with CompletableFuture to kick off asynchronous, non-blocking loading of the real subject as soon as the proxy is created, rather than blocking the first caller.

Instead of waiting for the first call to trigger construction, the proxy starts the expensive load eagerly but asynchronously in its constructor, storing a CompletableFuture; any method call then joins that future, which is instant if loading already finished by the time it's needed, or blocks only the remaining time if it's still in flight.

class AsyncVirtualProxy implements Image {
    private final CompletableFuture<RealImage> future;

    AsyncVirtualProxy(String path) {
        this.future = CompletableFuture.supplyAsync(() -> new RealImage(path));
    }

    @Override
    public void render() {
        future.join().render(); // instant if already loaded, otherwise waits out the remainder
    }
}
Watch out A bare .join() still blocks the calling thread if loading isn't finished; expose an async-friendly method too if callers can tolerate a genuinely non-blocking API.

21. Explain the protection proxy category in detail: how it enforces access control decisions in front of a real subject, and how this differs from scattering permission checks throughout business logic.

A protection proxy implements the same Subject interface as the real object and, before forwarding any call, evaluates whether the current caller is authorized to make it, throwing or denying if not. The real subject itself stays completely unaware of authorization; it simply does its job whenever it is called, trusting that any call reaching it has already been cleared.

This differs from scattering if (hasPermission(...)) checks inside the real class's own methods because the check is centralized in exactly one place, applies uniformly no matter how many methods the interface has, and can be swapped, tested, or disabled independently of the business logic it protects.

Centralized authorizationReal subject stays unaware

22. Implement a protection proxy in Java for a secured AdminOperations interface that only allows a call through to the real subject when the current user has the ADMIN role.

The proxy checks the caller's role against a SecurityContext before delegating; if the check fails, it throws immediately and the real subject's method body is never reached.

interface AdminOperations {
    void deleteUser(String userId);
}

class RealAdminOperations implements AdminOperations {
    public void deleteUser(String userId) { /* actually deletes */ }
}

class ProtectedAdminOperations implements AdminOperations {
    private final AdminOperations real;
    private final SecurityContext security;

    ProtectedAdminOperations(AdminOperations real, SecurityContext security) {
        this.real = real;
        this.security = security;
    }

    @Override
    public void deleteUser(String userId) {
        if (!security.currentUser().hasRole("ADMIN")) {
            throw new AccessDeniedException("ADMIN role required");
        }
        real.deleteUser(userId);
    }
}

23. Discuss how the Proxy pattern relates to Java's historical java.security.AccessController and SecurityManager mechanisms for enforcing permission checks around sensitive operations.

SecurityManager and AccessController acted as a systemic, JVM-wide protection proxy: sensitive operations, such as file access or reflection, routed through a permission check before the real operation executed, conceptually identical to a hand-written protection proxy but enforced platform-wide rather than per interface.

SecurityManager was deprecated for removal starting with Java 17 and removed entirely in later JDK releases, so modern code relies on explicit, application-level protection proxies (or a framework's security layer, such as Spring Security's method-security proxies) rather than the JVM-wide mechanism.

SecurityManager (removed)Application-level protection proxy

24. Design a feature-flagged proxy that transparently routes calls to either a new implementation or the legacy real subject depending on whether a feature flag is enabled for the current request.

The proxy implements the same PricingEngine interface as both candidate implementations and, on each call, consults a feature-flag service to decide which one to delegate to, letting the rollout be toggled per-tenant or per-percentage without any change to calling code.

class FeatureFlagPricingProxy implements PricingEngine {
    private final PricingEngine legacy;
    private final PricingEngine experimental;
    private final FeatureFlags flags;

    FeatureFlagPricingProxy(PricingEngine legacy, PricingEngine experimental, FeatureFlags flags) {
        this.legacy = legacy;
        this.experimental = experimental;
        this.flags = flags;
    }

    @Override
    public Price quote(Order order) {
        return flags.isEnabled("new-pricing", order.customerId())
            ? experimental.quote(order)
            : legacy.quote(order);
    }
}

25. Describe the security implications of a poorly implemented protection proxy, including a concrete example where a caller could bypass the check entirely, and how you would redesign the proxy to prevent it.

A protection proxy is only as strong as it is the sole path to the real subject. If the real subject is reachable through any other route, a public getter, a shared field, a factory that hands out the unwrapped instance, the check is decorative rather than enforced.

// Bad: exposes the unwrapped real subject alongside the protected one
class Container {
    public final AdminOperations protectedOps = new ProtectedAdminOperations(real, security);
    public final AdminOperations rawOps = real; // bypasses every check!
}
Redesign Never keep a reference to the real subject reachable from outside the proxy; construct it privately inside the proxy (or inject it through a constructor that nothing else holds) so the proxy is provably the only path in.

26. Compare using a protection proxy versus scattering an if (hasPermission(...)) check inline in every method of the business logic class, and explain when the proxy approach is worth the extra class.

Inline checks couple authorization logic to business logic in every method, forcing every future method to remember to add its own check and making it hard to see, audit, or unit test the authorization rules in isolation. A protection proxy consolidates every check into one class, visible in one place, and lets business logic be tested completely unaware of permissions.

The proxy is worth the extra class whenever the interface is stable and shared, or when authorization rules are non-trivial enough to want isolated testing; for a single throwaway internal method, an inline check may genuinely be simpler.

Centralized vs scatteredTestability

27. Describe how to combine a protection proxy with an audit-logging concern so that every denied access attempt is recorded, without duplicating the permission-check logic in two places.

Wrap the single permission check with a try/catch (or an early-return) that logs the denial before rethrowing, keeping the authorization decision itself in exactly one branch rather than duplicating the check once to decide and again to log.

@Override
public void deleteUser(String userId) {
    try {
        if (!security.currentUser().hasRole("ADMIN")) {
            throw new AccessDeniedException("ADMIN role required");
        }
        real.deleteUser(userId);
    } catch (AccessDeniedException ex) {
        auditLog.recordDenied(security.currentUser().id(), "deleteUser", userId);
        throw ex;
    }
}

28. Design a protection proxy for a multi-tenant SaaS application that ensures every call is scoped to the caller's own tenant, preventing cross-tenant data access even if the caller supplies another tenant's identifier.

The proxy compares the tenant identifier embedded in the current request context against the tenant identifier the caller is asking about, rejecting the call outright on any mismatch, rather than trusting a tenant identifier supplied directly in the request payload.

@Override
public Invoice getInvoice(String tenantId, String invoiceId) {
    if (!tenantId.equals(requestContext.currentTenantId())) {
        throw new AccessDeniedException("Cross-tenant access denied");
    }
    return real.getInvoice(tenantId, invoiceId);
}
Watch out Never derive the "current" tenant from a client-supplied field alone; it must come from an authenticated context the caller cannot forge.

29. Implement a protection proxy in front of a FileStore interface that restricts which file paths a given caller is allowed to read or write, rejecting any path outside an allow-listed directory.

The proxy normalizes the requested path first, resolving any .. segments, then checks that the normalized, absolute path still starts with the allowed root directory before delegating; checking the raw, unnormalized string is not enough to stop a path-traversal attempt.

@Override
public byte[] read(String requestedPath) {
    Path resolved = allowedRoot.resolve(requestedPath).normalize();
    if (!resolved.startsWith(allowedRoot)) {
        throw new AccessDeniedException("Path escapes allowed root: " + requestedPath);
    }
    return real.read(resolved.toString());
}
Path traversal Always normalize() before the startsWith check; comparing the raw string lets a request like ../../etc/passwd slip through undetected.

30. Describe how to unit test a protection proxy to verify it both correctly denies unauthorized callers and correctly allows authorized callers through to the real subject, without needing a real security infrastructure.

Mock the security context to return each role scenario, then assert both outcomes: that an unauthorized call throws and never reaches the real subject (verified via verify(real, never())), and that an authorized call succeeds and does reach it.

@Test
void deniesNonAdminAndNeverCallsRealSubject() {
    AdminOperations real = mock(AdminOperations.class);
    SecurityContext security = mock(SecurityContext.class);
    when(security.currentUser()).thenReturn(new User("bob", Set.of("USER")));
    ProtectedAdminOperations proxy = new ProtectedAdminOperations(real, security);

    assertThrows(AccessDeniedException.class, () -> proxy.deleteUser("u1"));
    verify(real, never()).deleteUser(any());
}

31. Explain the remote proxy category of the Proxy pattern: how it hides the fact that the real subject lives in a different process or machine, and what responsibilities the proxy takes on that a purely local proxy does not.

A remote proxy implements the same interface the real subject would expose if it were local, but every method internally performs network communication, serializing arguments, sending them to the remote process, and deserializing the response, all hidden from the caller behind an ordinary-looking method call.

Beyond a local proxy's responsibilities, a remote proxy must also handle concerns that only exist because a network sits between caller and callee: connection management, serialization format, timeouts, retries, and partial failure, none of which a purely in-process proxy needs to think about.

Network hidden behind local callMarshallingPartial failure

32. Describe the classic Java RMI stub-and-skeleton architecture and explain precisely how the client-side stub functions as a remote proxy implementing the same remote interface as the server-side object.

In RMI, both the client's stub and the server's actual implementation implement the same Remote interface. The client only ever holds the stub, generated to match that interface, and calling a method on it serializes the arguments, sends them over a socket to the server, and blocks waiting for the response, which the stub deserializes and returns as if the call had been local.

The server-side skeleton (largely folded into the runtime in later RMI versions) receives the incoming call, deserializes the arguments, invokes the real implementation object, and serializes the result back, mirroring the client stub's responsibilities on the other side of the wire.

RMI stub = remote proxy

33. Explain how a gRPC-generated client stub is a modern instance of the remote proxy pattern, and compare it to the older RMI stub in terms of how the interface is defined and how the proxy code is produced.

A gRPC client stub, generated from a .proto service definition, plays exactly the same remote proxy role as an RMI stub: the caller invokes what looks like an ordinary method, and the generated stub handles Protobuf serialization and an HTTP/2 request under the hood. The key difference is that the contract is defined language-neutrally in the .proto file rather than as a Java Remote interface, so the same service can generate proxy stubs for many languages, not just Java.

RMI's stub generation was Java-specific and tied to Java serialization; gRPC's code generation is cross-language and uses a compact binary wire format, but conceptually both are the exact same remote proxy idea: a generated class implementing the service's interface that hides the network call.

34. Implement a hand-written remote proxy in Java that implements a local-looking InventoryService interface but internally makes an HTTP call to a remote inventory microservice.

The proxy exposes the exact same domain method signature the rest of the application already calls locally, and internally builds the HTTP request, sends it, and maps the JSON response back onto the domain type, translating any transport-level failure into the domain's own exception type.

class HttpInventoryServiceProxy implements InventoryService {
    private final HttpClient client;
    private final URI baseUri;

    HttpInventoryServiceProxy(HttpClient client, URI baseUri) {
        this.client = client;
        this.baseUri = baseUri;
    }

    @Override
    public StockLevel getStock(String sku) {
        HttpRequest request = HttpRequest.newBuilder(baseUri.resolve("/stock/" + sku)).GET().build();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            return StockLevel.fromJson(response.body());
        } catch (IOException | InterruptedException ex) {
            throw new InventoryLookupException(sku, ex);
        }
    }
}

35. Describe how a remote proxy should handle network failures, timeouts, and retries so that transient issues are absorbed transparently without hiding failures the caller genuinely needs to know about.

Attach an explicit timeout to every outbound call so a hung remote never blocks the caller indefinitely, and retry only failures known to be transient and safe to repeat, such as connection timeouts on an idempotent read, never a write whose effect on the server is unknown.

HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(3)).GET().build();
Never retry blindly Retrying a non-idempotent write after a timeout risks executing it twice; only retry operations that are safely repeatable, or that carry an idempotency key the server can deduplicate on.

36. Explain what marshalling and unmarshalling responsibilities a remote proxy takes on, and why keeping this logic entirely inside the proxy is important for the rest of the codebase.

Marshalling converts domain-typed method arguments into whatever wire format the remote call requires, JSON, Protobuf, or a Java-serialized stream, and unmarshalling reverses that on the response. Keeping this entirely inside the proxy means the domain classes never need to know anything about the wire format, so switching from JSON over HTTP to Protobuf over gRPC later only touches the proxy, not any caller.

37. Discuss how a remote proxy's inherent latency and possibility of partial failure differ fundamentally from a purely in-process local proxy, and what this means for how callers should be coded even though the interface looks identical.

A local call essentially always succeeds and returns in nanoseconds; a remote call can be slow, can fail halfway through (the request might have been received and processed even though the response never arrived), and can fail in ways a local call structurally cannot, such as a network partition. The Subject interface looking identical is exactly what makes this dangerous: it tempts callers into treating a remote proxy call as if it carries the same reliability guarantees as a local one.

Design implication Callers of a remote proxy should still design for timeouts, retries-with-idempotency, and graceful degradation, even though nothing in the method signature signals that the call crosses a network.

38. Describe how to wrap a circuit breaker around the network call inside a remote proxy so that a flaky downstream dependency fails fast instead of exhausting caller threads.

The circuit breaker lives entirely inside the proxy, tracking recent failure rates for the wrapped call; once failures cross a configured threshold it opens, and further calls fail immediately with a domain exception instead of waiting on a slow or repeatedly failing remote, until a cool-down period allows a trial call through again.

class ResilientInventoryProxy implements InventoryService {
    private final CircuitBreaker breaker = CircuitBreaker.ofDefaults("inventory-service");
    private final InventoryService remote;

    ResilientInventoryProxy(InventoryService remote) { this.remote = remote; }

    @Override
    public StockLevel getStock(String sku) {
        try {
            return breaker.executeSupplier(() -> remote.getStock(sku));
        } catch (CallNotPermittedException ex) {
            throw new InventoryUnavailableException(sku, ex); // breaker is open
        }
    }
}
Circuit breakerFail fast

39. Design a database connection pooling proxy, similar to what HikariCP provides, that implements the same java.sql.Connection interface but transparently returns the physical connection to the pool instead of actually closing it when close() is called.

Application code calls connection.close() exactly as it always would, but the pooled proxy intercepts that one method and releases the underlying physical connection back into the pool instead of tearing down the socket; every other method delegates straight through to the physical connection unchanged.

class PooledConnectionProxy implements Connection {
    private final Connection physical;
    private final ConnectionPool pool;

    PooledConnectionProxy(Connection physical, ConnectionPool pool) {
        this.physical = physical;
        this.pool = pool;
    }

    @Override
    public void close() {
        pool.release(physical); // intercepted: returned to the pool, not physically closed
    }

    @Override
    public Statement createStatement() throws SQLException {
        return physical.createStatement(); // everything else delegates straight through
    }
    // ... remaining Connection methods delegate to `physical` unchanged
}
Connection poolingSmart reference

40. Compare the classic RMI remote proxy model to a modern service mesh sidecar proxy such as an Envoy sidecar, and explain what role, if any, the GoF Proxy pattern still plays at that infrastructure layer.

RMI's remote proxy lives inside the calling process as a generated Java object implementing the same interface as the remote service. A service mesh sidecar instead runs as a separate process alongside each service instance, intercepting network traffic transparently at the infrastructure layer rather than the object layer, with no generated client-side class required at all.

The GoF Proxy pattern's intent, standing in for another object and controlling access to it, still applies conceptually: the sidecar controls access to the real destination service (retries, circuit breaking, mTLS, routing) exactly as a remote proxy would, just implemented at the network layer instead of as an in-process object implementing a Java interface.

41. Explain the caching proxy as a specialization of the smart reference proxy category: how it decides whether to forward a call to the real subject at all, rather than merely how it forwards the call.

A caching proxy stores previously computed results keyed by call arguments and, before delegating, checks whether a fresh-enough result already exists; if it does, the proxy returns the cached value without ever touching the real subject at all. This is what sets it apart from most other proxy categories: the real call can be skipped entirely, not merely wrapped.

This also distinguishes it from a lazy virtual proxy, which defers only the first construction of the real subject; a caching proxy can potentially skip the real call on every subsequent invocation with matching arguments, for as long as the cached entry stays valid.

interface PriceLookup {
    Price lookup(String sku);
}

class CachingPriceLookupProxy implements PriceLookup {
    private final PriceLookup real;
    private final Map<String, Price> cache = new ConcurrentHashMap<>();

    CachingPriceLookupProxy(PriceLookup real) { this.real = real; }

    @Override
    public Price lookup(String sku) {
        return cache.computeIfAbsent(sku, real::lookup);
    }
}
Smart reference specializationCan skip the real call entirely

42. Implement a generic memoizing proxy in Java for a pure, deterministic computation, and explain why memoization is only safe when the wrapped call has no side effects and always returns the same result for the same arguments.

Memoization is caching applied specifically to pure functions: because the computation is deterministic and free of side effects, caching its result for a given input is always correct no matter how many times, or in what order, callers invoke it.

interface Calculator {
    long factorial(int n);
}

class RealCalculator implements Calculator {
    public long factorial(int n) {
        long result = 1;
        for (int i = 2; i <= n; i++) result *= i;
        return result;
    }
}

class MemoizingCalculatorProxy implements Calculator {
    private final Calculator real;
    private final Map<Integer, Long> cache = new ConcurrentHashMap<>();

    MemoizingCalculatorProxy(Calculator real) { this.real = real; }

    @Override
    public long factorial(int n) {
        return cache.computeIfAbsent(n, real::factorial);
    }
}
Not safe for side effects If the wrapped call sends an email, writes an audit row, or reads a clock or random source, memoizing it silently skips those effects (or freezes a stale reading) on every cache hit, which is a correctness bug, not a performance win.

43. Discuss the cache invalidation edge cases a caching proxy must handle: time-based expiry, explicit invalidation on writes, and the risk of serving stale data indefinitely if neither is implemented.

A caching proxy with no expiry and no explicit invalidation path simply keeps returning whatever it first computed, forever, even after the underlying data changes. The two main strategies are time-based expiry (each entry carries a timestamp and is considered stale after a fixed TTL) and explicit invalidation (the write path that changes the underlying data also tells the cache to drop or refresh the affected key).

class TtlCachingProxy implements PriceLookup {
    private record Entry(Price value, Instant expiresAt) {}
    private final PriceLookup real;
    private final Duration ttl;
    private final Map<String, Entry> cache = new ConcurrentHashMap<>();

    TtlCachingProxy(PriceLookup real, Duration ttl) {
        this.real = real;
        this.ttl = ttl;
    }

    @Override
    public Price lookup(String sku) {
        Entry entry = cache.get(sku);
        if (entry != null && Instant.now().isBefore(entry.expiresAt())) {
            return entry.value();
        }
        Price fresh = real.lookup(sku);
        cache.put(sku, new Entry(fresh, Instant.now().plus(ttl)));
        return fresh;
    }
}

Edge cases worth naming explicitly: invalidating a key while another thread is mid-read of it, a write path that lives in a different service and has no way to reach this proxy's cache at all (cross-service invalidation lag), and derived or aggregate cache entries that need invalidating whenever any of several underlying keys changes.

44. Explain how to make a caching proxy safe under concurrent access without serializing every call behind a single lock, and describe the correctness/performance trade-off with naive synchronization.

Synchronizing the entire lookup method serializes every caller, including cache hits, which defeats much of the point of caching under load: the exact moment the cache is working best is the moment you least want every thread queued behind one lock.

ConcurrentHashMap.computeIfAbsent is a better fit because it locks only the affected bucket, not the whole map, so unrelated keys never contend. The catch is that its mapping function must never itself try to update the same map, since a reentrant call into computeIfAbsent for the same map from inside the mapping function is undefined behavior and can throw or effectively deadlock in some JDK versions.

// Dangerous: mapping function mutates the same map it's computing into
cache.computeIfAbsent(sku, k -> {
    cache.put(relatedKey, precomputeRelated(k)); // re-entrant modification, avoid this
    return real.lookup(k);
});
Watch out Keep the mapping function passed to computeIfAbsent free of any modification to the same map; compute related entries separately, after the call returns.

45. Explain the cache stampede (thundering herd) problem that occurs when a hot cache entry expires and many concurrent requests recompute it simultaneously, and describe at least two mitigations a caching proxy can implement.

The moment a popular entry expires, many concurrent callers can all observe a miss at once and all call through to the real subject simultaneously, overwhelming exactly the expensive resource the cache existed to protect, with a burst of duplicate work computing the identical result.

class CoalescingCachingProxy implements PriceLookup {
    private final PriceLookup real;
    private final Map<String, CompletableFuture<Price>> inFlight = new ConcurrentHashMap<>();

    @Override
    public Price lookup(String sku) {
        CompletableFuture<Price> future = inFlight.computeIfAbsent(sku,
            k -> CompletableFuture.supplyAsync(() -> real.lookup(k)));
        try {
            return future.get();
        } finally {
            inFlight.remove(sku, future); // clear once the shared computation finishes
        }
    }
}

Two mitigations: request coalescing, where every concurrent caller for the same key awaits a single in-flight computation instead of starting their own, and stale-while-revalidate, where the just-expired value is served immediately while a refresh happens asynchronously in the background.

46. Is a caching wrapper more accurately classified as a Proxy or a Decorator, given the classic interviewer gotcha framing? Justify your answer using each pattern's defining intent rather than its structural shape.

A caching wrapper is a Proxy, not a Decorator. Its defining behavior is that it can skip the real subject entirely on a cache hit, it controls whether the real call happens at all, which is exactly Proxy's defining trait. A Decorator's defining trait is that it always forwards to the wrapped object and adds behavior around that unavoidable call; it never has the option of simply not calling through.

Structurally the two are identical, both implement the same interface and hold a reference to another instance of it, so the classification has to come from behavior and intent rather than code shape. A caching wrapper that always calls through anyway, for example one that also logs timing on every call regardless of hit or miss, starts to blur toward Decorator-flavored behavior, and acknowledging that nuance out loud is a stronger interview answer than reciting a rule mechanically.

Can skip the call = ProxyAlways forwards = Decorator

47. Implement a bounded caching proxy using an LRU (least-recently-used) eviction policy so unbounded cache growth doesn't quietly turn a caching proxy into a memory leak.

A LinkedHashMap constructed in access-order mode, with removeEldestEntry overridden to enforce a maximum size, is the simplest correct LRU implementation available in the standard library.

class LruCachingProxy implements PriceLookup {
    private final PriceLookup real;
    private final Map<String, Price> cache;

    LruCachingProxy(PriceLookup real, int maxEntries) {
        this.real = real;
        this.cache = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Price> eldest) {
                return size() > maxEntries;
            }
        });
    }

    @Override
    public Price lookup(String sku) {
        Price cached = cache.get(sku);
        if (cached != null) return cached;
        Price fresh = real.lookup(sku);
        cache.put(sku, fresh);
        return fresh;
    }
}

In production, a purpose-built library such as Caffeine is generally preferable, it handles size- or weight-based eviction, TTL, and concurrent access far more efficiently than a synchronized LinkedHashMap.

48. Design a caching proxy backed by a distributed cache such as Redis instead of an in-process map, and explain what changes when the cache is shared across multiple JVM instances.

Instead of a local Map, the proxy issues a GET against Redis before delegating, and a SET on a miss; the benefit is one shared, consistent cache across every horizontally scaled instance of a service, instead of each instance keeping its own duplicate, independently-warming copy.

@Override
public Price lookup(String sku) {
    String cached = redis.get(cacheKey(sku));
    if (cached != null) return Price.fromJson(cached);

    Price fresh = real.lookup(sku);
    try {
        redis.setex(cacheKey(sku), ttlSeconds, fresh.toJson());
    } catch (RedisConnectionException ex) {
        log.warn("Cache unavailable, skipping cache write for {}", sku, ex);
    }
    return fresh;
}
The cache is now a remote proxy too Reaching Redis is itself a network call that can fail or time out; treat the cache as an unreliable dependency and fall through to the real subject on a cache outage rather than letting a down cache take the whole feature down with it.

49. Describe common cache-key design mistakes in a caching proxy: keys that are too broad, too narrow, built from mutable objects, or that accidentally leak sensitive data, and explain the effect of each mistake.

A key that is too broad, one that omits a parameter that actually affects the result, causes two logically different requests to collide and return the wrong cached answer for one of them. A key that is too narrow, one that includes an irrelevant parameter such as a request id or timestamp, means every call produces a unique key, so the cache never hits at all and silently does nothing.

// Too narrow: requestId makes every key unique, defeating the cache entirely
Object[] badKey = { sku, requestId, Instant.now() };

// Correct: key contains exactly what determines the result, nothing else
String goodKey = sku;

Using a mutable object as a key without stable equals/hashCode is another trap: if the object mutates after being used as a key, a lookup with an "equal" but different instance may miss entirely, and mutating an object already stored as a key can corrupt the map's internal bucket structure outright. Finally, never bake sensitive values such as auth tokens directly into a cache key that might end up in logs or metrics tags.

50. Explain how Spring's @Cacheable annotation is implemented under the hood: how the framework turns a plain annotated method into caching-proxy behavior without you writing a proxy class yourself.

Spring wraps the annotated bean in a generated proxy, a JDK dynamic proxy if the bean implements an interface, or a CGLIB/ByteBuddy subclass proxy otherwise. The interceptor woven into that proxy checks the configured Cache abstraction for an entry keyed by the method's arguments before invoking the real method, and stores the result on a miss. Functionally this is exactly the caching proxy from Q41 and Q43, just generated by the framework instead of hand-written.

@Service
public class PriceService {
    @Cacheable("prices")
    public Price lookup(String sku) {
        return warehouse.queryPrice(sku); // only runs on a cache miss
    }
}
Framework-generated caching proxySubject to the self-invocation bug (Q75)

51. Implement a logging proxy that records the arguments and return value of every call to a PaymentGateway interface without modifying PaymentGateway's real implementation at all.

The proxy implements the exact same interface, logs before and after delegating, and returns whatever the real subject returned unchanged, so calling code cannot tell logging is happening at all beyond the log output itself.

class LoggingPaymentGatewayProxy implements PaymentGateway {
    private final PaymentGateway real;

    LoggingPaymentGatewayProxy(PaymentGateway real) { this.real = real; }

    @Override
    public Receipt charge(String customerId, BigDecimal amount) {
        System.out.println("Charging " + customerId + " for " + amount);
        Receipt receipt = real.charge(customerId, amount);
        System.out.println("Result: " + receipt);
        return receipt;
    }
}

52. Design a metrics-collecting proxy that records call latency and success/failure counts for every method call on a wrapped interface, suitable for exporting to a monitoring system such as Micrometer.

The proxy starts a timer sample before delegating and, in a finally block, records elapsed time and a success/failure tag, so metrics are captured consistently whether the real call succeeds, throws, or is slow, without the real subject needing any monitoring code of its own.

class MetricsPaymentGatewayProxy implements PaymentGateway {
    private final PaymentGateway real;
    private final MeterRegistry registry;

    @Override
    public Receipt charge(String customerId, BigDecimal amount) {
        Timer.Sample sample = Timer.start(registry);
        String outcome = "success";
        try {
            return real.charge(customerId, amount);
        } catch (RuntimeException ex) {
            outcome = "failure";
            throw ex;
        } finally {
            sample.stop(registry.timer("payment.charge", "outcome", outcome));
        }
    }
}

53. Implement a smart-reference proxy that reference-counts how many active clients are using a shared, expensive resource, and releases the underlying resource only once the count drops to zero.

Every client acquires through the proxy rather than holding the real resource directly; the proxy increments a counter on acquisition and decrements it on release, only actually closing the shared resource once the last client releases it.

class RefCountedResourceProxy implements SharedResource {
    private final SharedResource real;
    private final AtomicInteger refCount = new AtomicInteger(0);

    @Override
    public void acquire() {
        refCount.incrementAndGet();
    }

    @Override
    public void release() {
        if (refCount.decrementAndGet() == 0) {
            real.close(); // only the last releaser actually tears it down
        }
    }
}
Smart referenceBookkeeping around access

54. Explain the copy-on-write smart-reference proxy variant: how it lets multiple owners cheaply share a read-only view of the same underlying data and only pays the cost of duplicating it the moment one of them attempts to mutate it.

The proxy initially hands out the same shared underlying instance for every read operation, so many owners can share one copy at essentially no extra cost. The first mutating operation triggers an actual deep copy so that mutation never affects any other owner still holding the proxy; after that copy, the proxy transparently forwards further calls to its now-private copy instead of the originally shared instance.

class CopyOnWriteDocumentProxy implements Document {
    private Document target; // initially shared
    private boolean owned = false;

    @Override
    public void append(String text) {
        if (!owned) {
            target = target.copy(); // deep copy deferred until the first write
            owned = true;
        }
        target.append(text);
    }

    @Override
    public String read() {
        return target.read(); // reads never trigger a copy
    }
}
Smart referenceDeferred copy until first mutation

55. Design an audit-logging proxy for a compliance-sensitive BankAccountOperations interface that records who performed which mutating operation and when, distinct from the earlier denial-only audit logging on a protection proxy.

Where a protection proxy's audit logging (Q27) records only denied attempts, a compliance audit log needs a complete record of every successful mutating call as well, since regulators care about what actually happened, not merely what was blocked.

@Override
public void withdraw(String accountId, BigDecimal amount) {
    real.withdraw(accountId, amount);
    auditLog.record(new AuditEntry(security.currentUser().id(), "withdraw", accountId, amount, Instant.now()));
}
Compliance audit trailRecords success, not just denial

56. Implement a rate-limiting proxy using a token-bucket algorithm that rejects calls to a real subject once a caller exceeds an allowed request rate.

The proxy holds a bucket of tokens that refills at a fixed rate; each call attempts to consume one token before delegating, and if none remain, the call is rejected immediately rather than reaching the real subject, protecting it from bursts that exceed its capacity.

class RateLimitedApiProxy implements ExternalApi {
    private final ExternalApi real;
    private final Bucket bucket; // e.g. Bucket4j token bucket

    @Override
    public ApiResponse call(ApiRequest request) {
        if (!bucket.tryConsume(1)) {
            throw new RateLimitExceededException("Too many requests");
        }
        return real.call(request);
    }
}

57. Explain how multiple proxies, such as logging, caching, and rate-limiting, can be composed around a single real subject, and why the order in which they are layered changes the resulting behavior.

Because every proxy in the stack implements the same Subject interface and holds a reference to another instance of it, proxies can be nested arbitrarily deep: outer wraps middle wraps inner wraps the real subject. Since each layer only ever sees what the layer inside it returns (or throws), the order in which layers are stacked changes what actually happens on each call.

PaymentGateway gateway = new LoggingProxy(
    new RateLimitingProxy(
        new CachingProxy(realGateway)));

Placing the caching proxy outside the rate-limiting proxy means cache hits never consume a rate-limit token; placing it inside means every call, hit or miss, is rate-limited. Placing logging outermost logs every call attempt including ones rejected by an inner layer; placing it innermost logs only calls that survive every layer above it.

Decorator-style composition of proxiesOrder-dependent semantics

58. Compare the Null Object pattern to a Proxy that always denies access, and explain why they solve different problems even though both can stand in for a real subject a caller cannot actually use.

Null Object provides a harmless, do-nothing implementation of an interface so callers never need a null check; it does not wrap or communicate with any real subject at all, it is simply a safe default for the case where there is nothing real to use. A denying protection proxy, by contrast, does wrap a genuine real subject and actively decides, per call, whether a particular caller may reach it, typically throwing rather than quietly doing nothing.

A protection proxy that returns a harmless default instead of throwing for a denied caller starts to resemble a Null Object in effect, but the underlying intent stays distinct: Null Object exists to eliminate null-checking noise; Proxy exists for access control, which just as often needs to fail loudly and audibly rather than silently.

Null Object: no real subject at allProxy: wraps and gates a real subject

59. Implement a proxy that automatically retries a flaky remote call with exponential backoff before giving up and propagating the failure to the caller.

The proxy loops up to a maximum attempt count, sleeping for an increasing delay between attempts, and only lets the underlying exception escape to the caller once every retry has been exhausted.

class RetryingInventoryProxy implements InventoryService {
    private final InventoryService real;
    private final int maxAttempts;

    @Override
    public StockLevel getStock(String sku) {
        long delayMs = 100;
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return real.getStock(sku);
            } catch (InventoryLookupException ex) {
                if (attempt == maxAttempts) throw ex;
                sleepQuietly(delayMs);
                delayMs *= 2; // exponential backoff
            }
        }
        throw new IllegalStateException("unreachable");
    }
}

60. Design a proxy that consistently routes each user to either a control or experimental implementation of a RecommendationEngine interface for an A/B test, ensuring the same user always lands in the same variant across repeated calls.

Unlike the boolean feature-flag proxy in Q24, an A/B test needs each user's assignment to stay consistent across many calls and sessions so the resulting measurements are statistically valid. This is achieved by hashing a stable user identifier into a fixed bucket rather than re-deciding randomly on every call.

@Override
public List<Recommendation> recommend(String userId) {
    int bucket = Math.floorMod(userId.hashCode(), 100);
    return bucket < rolloutPercentage
        ? experimental.recommend(userId)
        : control.recommend(userId);
}

61. Explain in detail how java.lang.reflect.Proxy.newProxyInstance works: what class it generates at runtime, what that generated class extends and implements, and how every method call is routed to your InvocationHandler.

At runtime, Proxy.newProxyInstance generates (and caches for reuse) a new class that extends java.lang.reflect.Proxy and implements every interface passed to it. Every method declared by those interfaces is overridden in the generated class to do exactly one thing: package up the Method, its arguments, and the proxy instance itself, and forward them to invocationHandler.invoke(proxy, method, args), whose return value becomes the method's own return value.

AccountService proxy = (AccountService) Proxy.newProxyInstance(
    AccountService.class.getClassLoader(),
    new Class<?>[] { AccountService.class },
    (proxyObj, method, args) -> {
        System.out.println("Calling " + method.getName());
        return method.invoke(real, args);
    });
Generated class extends java.lang.reflect.ProxySingle InvocationHandler.invoke dispatch point

62. Implement a single, reusable InvocationHandler that adds caching behavior to any interface, keyed by the method called and its arguments, without writing a separate caching proxy class per interface.

Because every call already arrives at one generic invoke method with the Method and its arguments available, a single handler can key a cache on the method name plus its argument list and apply to any interface at all, not just one hand-picked type.

class CachingInvocationHandler implements InvocationHandler {
    private final Object real;
    private final Map<List<Object>, Object> cache = new ConcurrentHashMap<>();

    CachingInvocationHandler(Object real) { this.real = real; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        List<Object> key = List.of(method.getName(), args == null ? List.of() : Arrays.asList(args));
        return cache.computeIfAbsent(key, k -> {
            try {
                return method.invoke(real, args);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        });
    }
}
One handler, any interface

63. Explain precisely why java.lang.reflect.Proxy can only proxy interfaces and not concrete classes, tracing the limitation back to single inheritance in the JVM.

The generated proxy class must itself extend java.lang.reflect.Proxy to inherit the dispatch machinery that holds the reference to your InvocationHandler. Since Java classes support only single inheritance, that one superclass slot is already spent, leaving the generated class free only to implement additional interfaces, never to extend another concrete class as well.

This is exactly why CGLIB and ByteBuddy exist: they proxy a concrete class by generating a subclass of that class directly, which sidesteps the need to also extend java.lang.reflect.Proxy.

Single inheritance limitationCGLIB subclasses instead of implementing

64. Compare how java.lang.reflect.Proxy dispatches calls (via reflection) to how CGLIB/ByteBuddy-generated proxies dispatch calls (via generated bytecode), and explain the performance implication of each approach.

A JDK dynamic proxy's overridden methods still call back through reflective Method.invoke on the InvocationHandler side, paying reflection overhead, boxing arguments into an Object[], and making it harder for the JIT to inline across that reflective boundary. CGLIB and ByteBuddy instead generate an actual bytecode subclass whose overridden methods call directly into a MethodInterceptor, producing dispatch that is closer to an ordinary virtual call and avoiding boxing in many cases.

In practice this difference is usually dwarfed by whatever genuine I/O the intercepted method actually performs, and only becomes measurable in tight loops of otherwise trivial calls.

Reflective dispatch (JDK)Generated bytecode dispatch (CGLIB/ByteBuddy)

65. Explain how equals(), hashCode(), and toString() calls on a JDK dynamic proxy instance are actually handled, and describe a bug this causes when comparing a proxy instance to its real subject.

The generated proxy class overrides Object's equals, hashCode, and toString just like every other method, routing them through the same InvocationHandler.invoke. Unless your handler special-cases them, they typically default to identity-based behavior on the proxy instance itself, not the real subject it wraps.

@Override
public Object invoke(Object proxy, Method method, Object[] args) {
    if (method.getName().equals("equals") && args.length == 1) {
        return proxy == args[0]; // typical default: identity only, never equal to the real subject
    }
    // ... forward everything else
    return null;
}
Bug A proxy is never == its real subject and, unless equals() is explicitly forwarded, is never equals() to it either, so code expecting the two to collide in a Set or Map silently ends up with two distinct entries.

66. How would you measure whether the reflective dispatch overhead of a JDK dynamic proxy is actually significant in a given production code path, rather than assuming it is a bottleneck?

Profile the actual code path under realistic load with a proper tool such as async-profiler or Java Flight Recorder rather than guessing; reflection overhead is measured in nanoseconds per call and is dwarfed by almost any I/O, JSON parsing, or database work the intercepted method typically performs. It matters only in call-heavy inner loops doing genuinely trivial per-call work.

If profiling actually shows it matters, options are switching that path to a CGLIB/ByteBuddy generated proxy, hand-writing a static proxy for just that hot path, or confirming your own code isn't repeating a Method lookup on every call (the JDK already caches this internally).

Profile before optimizingRarely the actual bottleneck

67. Show how a single java.lang.reflect.Proxy instance can implement multiple unrelated interfaces at once, and describe a realistic use case for doing so.

Proxy.newProxyInstance accepts an array of interfaces, and the single generated class implements all of them; the resulting object can then be cast to whichever interface a particular piece of code needs.

Object proxy = Proxy.newProxyInstance(
    loader,
    new Class<?>[] { AccountService.class, java.io.Closeable.class },
    handler);

AccountService svc = (AccountService) proxy;
Closeable closeable = (Closeable) proxy;

A realistic use case is a resource-like proxy that needs to satisfy the domain interface callers already depend on while also implementing Closeable/AutoCloseable so it can participate in try-with-resources, without the real subject itself needing to implement both.

68. Explain what causes java.lang.reflect.UndeclaredThrowableException when using a JDK dynamic proxy, and how to avoid triggering it.

Each interface method declares a specific set of checked exceptions it is allowed to throw. If your InvocationHandler.invoke lets a checked exception escape that isn't declared on the interface method being called (and isn't a RuntimeException or Error), the generated proxy can't legally propagate it unchanged, so it wraps it in an unchecked UndeclaredThrowableException instead.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        return method.invoke(real, args);
    } catch (InvocationTargetException ex) {
        throw ex.getCause(); // unwrap so the *original* declared exception propagates correctly
    }
}
Most common cause Forgetting to unwrap InvocationTargetException.getCause() and letting the wrapper exception itself escape is the single most common source of this exception.

69. Describe how to unit test code that constructs a JDK dynamic proxy or a generic InvocationHandler, verifying the cross-cutting behavior it adds without re-testing the real subject's business logic.

Test against the interface exactly like any other consumer would, supplying a mock or stub real subject, and assert only the added behavior, that a call was cached, denied, retried, or logged, using interaction verification rather than re-asserting business logic that belongs to the real subject's own tests.

@Test
void cachesRepeatedCallsToSameArgument() {
    AccountService real = mock(AccountService.class);
    when(real.getBalance("a1")).thenReturn(new Balance("a1", 100));
    AccountService proxy = cachingProxy(AccountService.class, real);

    proxy.getBalance("a1");
    proxy.getBalance("a1");

    verify(real, times(1)).getBalance("a1"); // second call served from cache
}

70. Design a small, reusable proxy factory utility that can wrap any interface with a chosen cross-cutting InvocationHandler, so new caching, logging, or retry behavior can be added to any service interface without writing a new proxy class each time.

A single generic factory method captures the boilerplate of calling Proxy.newProxyInstance, leaving only the interface type and the desired handler as call-site concerns.

final class ProxyFactory {
    private ProxyFactory() {}

    @SuppressWarnings("unchecked")
    static <T> T wrap(Class<T> iface, InvocationHandler handler) {
        return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[] { iface }, handler);
    }
}

// usage: same factory, any cross-cutting behavior
AccountService logged = ProxyFactory.wrap(AccountService.class, (p, m, a) -> {
    System.out.println("Calling " + m.getName());
    return m.invoke(real, a);
});
One factory, many cross-cutting behaviors

71. Explain how CGLIB (and its modern successor ByteBuddy) implement proxying for concrete classes without an interface, by generating a runtime subclass, and show a minimal example.

Instead of implementing an interface like the JDK approach, CGLIB and ByteBuddy generate an actual subclass of the target class at runtime, overriding its non-final, accessible methods to route through a MethodInterceptor (CGLIB) or an equivalent callback (ByteBuddy), typically with the option to still call the original implementation via a generated method proxy.

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(RealReportGenerator.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
    System.out.println("Calling " + method.getName());
    return methodProxy.invokeSuper(obj, args);
});
ReportGenerator proxyInstance = (ReportGenerator) enhancer.create();
Subclassing, not interface implementation

72. Explain why CGLIB and ByteBuddy cannot proxy a final class, and why a final method on an otherwise proxyable class also cannot be intercepted, tracing both back to the subclassing mechanism.

Since CGLIB/ByteBuddy work by generating a subclass of the target, a final class cannot be extended at all, so proxy creation fails outright with an error at proxy-creation time. A final method, even on a non-final class, cannot be overridden by the generated subclass either, so calls to that specific method reach the original implementation directly, silently bypassing interception, no error is raised for the method itself; it simply never gets intercepted.

final class ImmutableConfig { /* CGLIB: cannot proxy this class at all */ }

class ReportService {
    public final void save() { /* CGLIB: this method is silently never intercepted */ }
}
Watch out A final method under a CGLIB/ByteBuddy proxy fails silently, no exception, no log line, just quietly unproxied behavior, making it one of the harder proxy bugs to spot in review.

73. Explain how Spring AOP decides, for a given bean, whether to generate a JDK dynamic proxy or a CGLIB/ByteBuddy subclass proxy, and how to force one or the other.

By default, Spring generates a JDK dynamic proxy if the target bean implements at least one interface, and falls back to a CGLIB/ByteBuddy subclass proxy only if it doesn't. Setting proxyTargetClass=true (Spring Boot's default) forces subclassing even when interfaces exist, commonly done so that autowiring by concrete class type keeps working.

@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig { }
Interface present -> JDK proxy (default)proxyTargetClass=true -> always subclass

74. Explain exactly how Spring implements @Transactional using a proxy: what the generated proxy does before and after the annotated method executes, and what happens if the method throws.

The proxy wrapping the bean begins a transaction (or joins an existing one, per the configured propagation) before calling through to the real method, commits it if the method returns normally, and rolls it back if the method throws an unchecked exception. Checked exceptions do not trigger a rollback by default unless explicitly configured to.

@Transactional
public void transferFunds(String from, String to, BigDecimal amount) {
    debit(from, amount);
    credit(to, amount); // if this throws (unchecked), the proxy rolls back the whole transaction
}
Default rollback rule Only unchecked exceptions trigger rollback by default; use @Transactional(rollbackFor = ...) to roll back on a checked exception too.

75. Explain the classic Spring self-invocation bug: why calling another @Transactional or @Cacheable method on this from within the same class silently bypasses the proxy's behavior entirely.

Spring's proxy wraps the bean from the outside; interception only happens for calls that arrive through that proxy. Calling another method via this from inside the same class is a plain, direct Java call on the real object, it never passes through the surrounding proxy, so no interception logic runs at all, silently.

@Service
class OrderService {
    public void placeOrder(Order order) {
        this.chargeCustomer(order); // BUG: direct call, bypasses the @Transactional proxy entirely
    }

    @Transactional
    public void chargeCustomer(Order order) { /* ... */ }
}
No exception is thrown The annotation is simply ignored for this call path, which is exactly what makes it so easy to miss in code review.

76. Describe at least two concrete ways to fix the Spring self-invocation bug so a proxied method's cross-cutting behavior actually applies when called from within the same class.

One fix is injecting the bean into itself, typically via @Lazy self-autowiring, and calling the annotated method through that injected proxy reference instead of this. Another is moving the annotated method into a separate collaborator bean and calling it through that bean's own proxy from the original caller. A less common option, AopContext.currentProxy() with exposeProxy=true, works but is generally considered a code smell.

@Service
class OrderService {
    @Lazy @Autowired private OrderService self; // the proxy, injected into itself

    public void placeOrder(Order order) {
        self.chargeCustomer(order); // now correctly goes through the proxy
    }

    @Transactional
    public void chargeCustomer(Order order) { /* ... */ }
}

77. A teammate marks a @Transactional method final for defensive reasons and is confused when transactions silently stop working. Diagnose the bug and explain the fix.

Because this bean lacks an interface here, Spring is generating a CGLIB/ByteBuddy subclass proxy, which cannot override a final method. The annotation is still parsed, but the interception never actually wraps that method, so it runs entirely unproxied, silently, the same failure mode as Q72 and Q75 but triggered by finality instead of self-invocation.

@Service
class OrderService {
    @Transactional
    public final void chargeCustomer(Order order) { /* silently never gets a transaction */ }
}

The fix is to remove final, or to extract the method behind an interface so Spring uses a JDK proxy instead of subclassing (interface methods can't be final anyway), or to move the method into a genuinely separate, non-final bean.

78. Produce a single comparison of JDK dynamic proxies, CGLIB, and ByteBuddy covering what each requires of the target, how each dispatches calls, and their current status in the Java ecosystem.

ApproachRequires of the targetDispatch mechanismEcosystem status
JDK dynamic proxyMust implement at least one interfaceReflective dispatch to InvocationHandler.invokePart of the JDK itself, no dependency needed
CGLIBNon-final class, accessible (or Objenesis-bypassed) constructorGenerated bytecode calling MethodInterceptorLargely unmaintained upstream; still bundled/shaded by some frameworks for compatibility
ByteBuddyNon-final class, same constructor caveat as CGLIBGenerated bytecode via a more modern, actively maintained APIActively maintained; Spring's default class-proxying mechanism since replacing CGLIB

79. Explain the bug that occurs when code casts a Spring-managed bean directly to its concrete implementation class, and why this cast sometimes works and sometimes throws ClassCastException depending on which proxy type Spring chose.

If Spring generated a JDK dynamic proxy, the proxy object only implements the declared interfaces, not the concrete class, so casting it to the concrete implementation class throws ClassCastException. If Spring instead generated a CGLIB/ByteBuddy subclass proxy, the cast happens to work because the proxy genuinely is a subclass of the concrete class, but depending on that is fragile, since it silently breaks the moment the proxy strategy changes, for example when an interface is added or a configuration flag flips.

AccountServiceImpl impl = (AccountServiceImpl) context.getBean(AccountService.class);
// works only if Spring happened to generate a CGLIB/ByteBuddy proxy for this bean
Fix Always depend on the interface type, never the concrete implementation type, when working with a potentially proxied bean.

80. Explain how Spring AOP's advice types, before, after, after-returning, after-throwing, and around, map onto the underlying proxy's method interception mechanism.

Every advice type is really a different slice of the same single interception point: the call arriving at the proxy before it reaches (or instead of) the real target's method. Around advice gets the fullest control, receiving a ProceedingJoinPoint and deciding for itself whether and when to call proceed(); before, after, after-returning, and after-throwing are conveniences layered on top that run their own logic at a fixed point relative to that same underlying proceed() call.

@Around("execution(* com.example.AccountService.*(..))")
public Object timeIt(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.nanoTime();
    try {
        return pjp.proceed(); // the same "call the real subject" step every proxy has
    } finally {
        System.out.println((System.nanoTime() - start) / 1_000_000 + "ms");
    }
}

81. Beyond the constructor-level leak discussed for protection proxies (Q25), describe how a Spring application can accidentally leak the unwrapped real bean around a proxy, and why this defeats @Transactional, @Cacheable, or security advice entirely for the leaked reference.

A common route is a bean capturing this into a static field or a manually-populated registry inside @PostConstruct, before the container has necessarily finished wrapping it, or code that obtains the raw target through reflection in a way that escapes the container's own bean-management abstraction. Any caller that later reads that captured reference bypasses every proxy-based concern for it, silently, the exact same underlying failure as self-invocation, just reached through a different escape route.

@Component
class Registry {
    static OrderService instance; // captured raw, potentially before proxying finishes
}

@Service
class OrderServiceImpl implements OrderService {
    @PostConstruct
    void register() {
        Registry.instance = this; // leaks the unwrapped bean, bypassing @Transactional forever
    }
}

82. Beyond a caller instanceof-checking a proxy (Q10), describe how a dynamic or AOP proxy can break substitutability more subtly, by changing which exceptions propagate or how results are represented compared to the real subject.

A caching InvocationHandler that catches every exception from the real call and returns a cached-null sentinel instead of rethrowing changes the contract callers rely on. A retry proxy that ultimately propagates a wrapped, generic failure type where the real subject would have thrown its own specific exception type breaks catch blocks written against that original type.

// Bad: swallows the real exception and silently returns null instead
try {
    return real.getBalance(accountId);
} catch (Exception ex) {
    return null; // caller now sees a different contract than the real subject ever had
}
Rule of thumb A cross-cutting proxy should preserve the original exception type (or apply a deliberate, documented translation), never paper over a failure with a shape the interface's own contract never promised.

83. In a hand-written, non-dynamic proxy class, what bug occurs if the proxy forgets to override equals() and hashCode(), and it is later placed into a HashSet or used as a Map key expecting equality with its real subject?

Without an override, the proxy inherits Object's identity-based equals/hashCode, so two separate proxy instances wrapping the exact same real subject are never equals() to each other, and neither is equals() to the real subject itself. Code that expects "logically the same account" to collide in a Set silently ends up storing duplicate entries instead.

Set<AccountService> seen = new HashSet<>();
seen.add(realAccountService);
seen.add(new AccountServiceProxy(realAccountService));
// seen.size() == 2, not 1, because neither equals() nor hashCode() was overridden

Forward equals/hashCode to the real subject (or to a stable identifier) explicitly whenever identity-based equality isn't the semantic you actually want.

84. Explain what happens when you attempt to serialize a JDK dynamic proxy instance with standard Java serialization, and what is required for it to actually work, versus attempting to serialize a CGLIB/ByteBuddy subclass proxy.

A JDK dynamic proxy can be serialized only if every interface it implements is Serializable and its InvocationHandler is also Serializable; if either condition fails, serialization throws NotSerializableException at the point it reaches the offending part. A CGLIB/ByteBuddy subclass proxy is generally much harder to serialize reliably, since the generated subclass, its intercepted fields, and its callback wiring are runtime artifacts not designed to survive serialization and deserialization reliably across JVMs, or even across separate runs where the generated class is regenerated differently.

Practical advice Avoid serializing proxies directly; serialize the underlying data or a DTO instead, and reconstruct or re-wrap it fresh on the receiving side.

85. Describe why stack traces through several layered dynamic or AOP proxies become hard to read, filled with generated proxy classes and reflective dispatch frames, and how to mitigate this during debugging.

Each layer contributes its own generated proxy class frame plus its InvocationHandler.invoke or MethodInterceptor.intercept frame, and JDK proxies add a reflective Method.invoke frame on top of that, so a call passing through three stacked proxies (Q57) before reaching the real subject can add a dozen or more noisy frames between the caller and the actual failure.

Mitigations: name InvocationHandler/interceptor implementations descriptively rather than as anonymous lambdas so they're identifiable in a trace, keep translation logic in each layer minimal so exceptions aren't needlessly re-wrapped and adding yet more frames, and use the IDE's framework-frame-collapsing feature while stepping through a stack.

86. Describe a deadlock scenario where a real subject, invoked through a synchronized caching proxy, calls back into the same proxy from a different thread, and explain why this differs from the reentrant-safe behavior of Java's own synchronized keyword.

Java's synchronized is reentrant: a thread re-entering a lock it already holds never deadlocks itself. The danger appears when the real subject's work is dispatched to another thread, for example submitted to a fixed-size executor, and that other thread's task needs to call back into the same synchronized proxy method to read a value currently being computed: if the executor has no free thread to run that callback, and the original call is meanwhile blocked waiting on the callback's result, the two calls deadlock waiting on each other, even though no single thread ever tries to re-acquire a lock it already holds.

ExecutorService singleThreadPool = Executors.newFixedThreadPool(1);

// Submitting work from within the pool's own only thread, then blocking on its result, deadlocks
singleThreadPool.submit(() -> {
    Future<String> inner = singleThreadPool.submit(() -> "value");
    return inner.get(); // never runs: no free thread left to service it
});
Root cause This is thread-pool exhaustion, not lock re-entrancy; separating the pools or restructuring so a callback never blocks on the same pool it was submitted from is the fix.

87. A caching proxy that seemed fine in testing causes an OutOfMemoryError in production after running for days. Diagnose the likely cause and the fix, building on the eviction policy discussed for LRU caches.

An unbounded Map used as a cache, one with no TTL, no LRU eviction, and no size cap, accumulates one entry per distinct key ever seen. In production, with far more distinct arguments over days than any test run exercises, this map grows without bound until the heap is exhausted. The fix is exactly the bounded LRU cache (Q47) or TTL-based expiry (Q43) that a quick prototype often skips entirely, or adopting a purpose-built library like Caffeine that enforces a maximum size or weight by default.

Subtler variant Even a bounded cache leaks if cached values themselves hold references to large objects, such as an entire request context, that should have been released; bounding entry count alone doesn't bound total memory if entry size varies wildly.

88. Explain the risk of interface/implementation version skew for a remote proxy, such as a generated RMI stub or a gRPC client stub, when the server-side contract evolves independently of when every client regenerates its stub.

A generated client stub is compiled against a specific version of the shared contract, the Remote interface or the .proto file. If the server adds a field or method and redeploys before every client regenerates and redeploys its own stub, older stubs keep calling against the old contract shape; depending on the framework's compatibility rules this either works transparently (additive, backward-compatible Protobuf field additions), fails outright at the transport layer, or silently loses information the new server tries to communicate that the old stub was never built to read.

Mitigation Only additive, backward-compatible changes to a widely-deployed contract, plus explicit versioning and deprecation windows for any genuinely breaking change, exactly like versioning any other public API.

89. Explain why CGLIB (and, historically, some ByteBuddy usages) required the proxied class to have an accessible no-argument constructor, and how tools like Objenesis worked around this requirement.

Since CGLIB creates a proxy by instantiating a generated subclass of the target class, and constructing any Java subclass instance normally requires running some constructor of its superclass, CGLIB by default needs an accessible no-arg constructor on the class it is subclassing in order to actually create an instance of the generated subclass.

Objenesis, used internally by frameworks such as Spring and Mockito, sidesteps this entirely by allocating the object's memory directly through JVM-internal APIs, without running any constructor at all, which is why proxies (and mocks) can be created even for classes whose only constructor requires arguments or does non-trivial work in its body.

Objenesis bypasses constructors entirely

90. Explain the fail-open versus fail-closed distinction for a protection proxy's error handling, and why a proxy that defaults to allowing access when an unexpected error occurs during the permission check is a serious security bug.

Fail-closed means any error, exception, timeout, or ambiguous result during the authorization check is treated as denied by default; fail-open means the same error is treated as allowed by default. A protection proxy should always fail closed, because the entire reason it exists is that unauthorized access must never slip through, and an unexpected error is exactly the situation where the check is least trustworthy.

try {
    if (security.currentUser().hasRole("ADMIN")) {
        real.deleteUser(userId);
    }
} catch (Exception ex) {
    real.deleteUser(userId); // BUG: fails open on any error, including a broken security context
}
Fix Any error in the authorization path should re-throw or deny; it must never fall through to calling the real subject.

91. Summarize an overall testing strategy for a codebase that uses several kinds of proxies, virtual, protection, remote, caching, and dynamic/AOP, describing what to test at each layer and what not to duplicate.

Test the real subject's business logic in isolation, with no proxy involved at all. Test each proxy's specific added behavior, lazy init happens once, denial actually denies and never reaches the real subject, cache hits skip the real subject, retries stop after a bounded number of attempts, against a mocked or faked real subject, verifying interaction counts rather than re-testing business logic that already has its own tests.

Reserve a small number of true integration tests to confirm the fully composed stack, for example Spring's actual generated proxy plus real beans, behaves correctly end-to-end, since isolated unit tests around individual proxies cannot catch composition-order bugs (Q57) or framework wiring mistakes like self-invocation or final methods (Q75, Q77).

92. Go deeper than a Connection.close() override (Q39): describe how a production-grade connection-pooling proxy such as HikariCP's also wraps Statement and ResultSet objects, and why that additional wrapping matters for leak detection.

A production pool's Connection proxy doesn't just intercept close(); it also wraps every Statement/PreparedStatement it creates in its own proxy, tracking which statements a given pooled connection currently has open. When the connection is returned to the pool, any statement the caller forgot to close can be force-closed and logged as a leak, rather than silently held open against a connection that's already back in the pool and handed to a different caller.

Statement/ResultSet also wrappedLeak detection at return-to-pool time

93. Distinguish Hibernate's lazy-collection proxies (such as PersistentBag or PersistentSet wrapping a one-to-many association) from the lazy entity proxies discussed earlier (Q13), and describe fixes specific to collection associations.

A lazy entity association proxy (Q13) stands in for a single related entity and initializes by loading exactly that one row. A lazy collection proxy wraps an entire associated collection and, when first touched, triggers a query loading every row in that one-to-many relationship, a different and often much larger cost profile than a single-entity proxy.

List<Order> orders = entityManager.createQuery(
        "select o from Order o join fetch o.lineItems where o.status = :status", Order.class)
    .setParameter("status", "OPEN")
    .getResultList(); // one query; lineItems is already initialized, no further per-order query

Fixes specific to collections: @BatchSize on the collection mapping to batch-initialize several parents' collections in one IN-clause query instead of one per parent, an entity graph or JOIN FETCH for exactly the collections a given query needs, and avoiding the classic mistake of eagerly fetching two or more separate collections in a single JOIN FETCH query, which multiplies rows in a Cartesian-product fashion.

94. Describe how the Proxy pattern can be combined with the Observer pattern so that intercepting a call through a proxy also publishes a notification to interested listeners whenever the real subject's state changes.

The proxy sits in front of every mutating call, so it is a natural place to fire notification events: after delegating a mutating method to the real subject, it walks its list of registered observers and notifies them, without the real subject's own code needing to know anything about the observer mechanism at all.

class ObservableAccountProxy implements AccountService {
    private final AccountService real;
    private final List<BalanceListener> listeners = new CopyOnWriteArrayList<>();

    @Override
    public void withdraw(String accountId, BigDecimal amount) {
        real.withdraw(accountId, amount);
        Balance updated = real.getBalance(accountId);
        listeners.forEach(l -> l.onBalanceChanged(accountId, updated));
    }
}
Proxy sees every callNatural notification point

95. Describe how the Proxy pattern can be combined with the Command pattern to implement a write-behind proxy that queues mutating calls as Command objects for asynchronous, batched execution instead of applying them immediately.

Instead of forwarding a mutating call straight to the real subject, the proxy wraps the intended operation and its arguments into a queued command and appends it to an internal queue, returning to the caller immediately; a background worker later drains that queue, replaying each command against the real subject, batching several together where the real subject supports it.

class WriteBehindOrderProxy implements OrderRepository {
    private final BlockingQueue<Runnable> pending = new LinkedBlockingQueue<>();
    private final OrderRepository real;

    @Override
    public void save(Order order) {
        pending.offer(() -> real.save(order)); // queued command, applied later by a worker thread
    }
}
Trade-off Callers relying on save() having taken effect immediately, for example reading it back right after, will be surprised; write-behind trades immediate consistency for throughput, and that trade-off must be explicit to callers.

96. Describe the internal architecture of a service mesh sidecar proxy such as Envoy: how traffic is transparently redirected into it, and how it decides where and how to route each intercepted call.

Each service instance runs its own sidecar proxy process alongside it, and outbound and inbound traffic is transparently redirected into that sidecar, typically via iptables rules or an equivalent traffic-interception mechanism, rather than any change to the application's own code. The application never explicitly calls the sidecar; it calls the destination address as it always would, and the network layer routes that call through the sidecar first.

The sidecar's routing, retry, timeout, and mTLS behavior is itself dynamically configured by a control plane pushing configuration over an API such as Envoy's xDS, so what the sidecar does with an intercepted call, which real destination it forwards to, what to retry, what to encrypt, can change live without redeploying the application at all, unlike a compiled-in client stub whose behavior is fixed at build time.

Transparent traffic interceptionControl plane pushes routing config live

97. An interviewer pushes back: "Isn't every wrapper class technically a proxy, since it forwards calls to another object?" How would you respond precisely, distinguishing the colloquial and the GoF-specific senses of the word?

Colloquially, yes: any object that holds and forwards to another object is a "wrapper" in the loose sense, and Adapter, Decorator, Facade, and Proxy are all wrappers under that loose definition. But the GoF Proxy pattern is a specific, narrower claim: the same interface as the thing wrapped, and the wrapping exists specifically to control access, not to translate an interface (Adapter), add stacked behavior (Decorator), or simplify a whole subsystem (Facade).

A strong answer acknowledges the structural overlap the interviewer is pointing at, then draws the line on intent: structurally, yes, they're all wrappers; the GoF names distinguish them by why the wrapping exists, and calling every wrapper a "proxy" loses exactly the information the pattern name is supposed to communicate to the next developer who reads the code.

98. Describe how to correctly benchmark the overhead of a JDK dynamic proxy versus a CGLIB/ByteBuddy proxy versus a direct call using JMH, and the common measurement mistakes that produce misleading results.

Use a proper microbenchmarking harness such as JMH rather than a hand-rolled loop with System.nanoTime(). Naive loops are vulnerable to JIT warmup not being accounted for, dead-code elimination (the JIT discovering the benchmarked call's result is never used and optimizing the whole call away), and constant-folding when arguments never vary between iterations.

@Benchmark
public Balance directCall(BenchmarkState state) {
    return state.real.getBalance("acct-1");
}

@Benchmark
public Balance jdkProxyCall(BenchmarkState state) {
    return state.jdkProxy.getBalance("acct-1");
}
Watch out Always consume the returned value (JMH's Blackhole) and vary inputs across iterations, or the JIT may eliminate the very call being measured, making "proxy overhead" look like zero for the wrong reason.

99. Describe a strangler-fig style legacy migration where a Facade unifies the old and new subsystem's interface for callers, while an internal Proxy decides, per call, whether to route to the legacy implementation or the new one.

The Facade gives every caller one stable, simplified entry point so they never need to know a migration is happening at all. Behind that facade, a routing proxy implementing the same fine-grained interface as both the legacy and new subsystems decides per call, per feature flag, per tenant, or per gradually increasing rollout percentage, which underlying implementation actually handles the request; this is exactly the feature-flagged proxy from Q24, now sitting behind a Facade instead of being called directly.

class OrderFacade {
    private final OrderOperations routingProxy; // Proxy: decides legacy vs. new per call

    public OrderSummary placeOrder(OrderRequest request) {
        return routingProxy.place(request).toSummary(); // Facade: one simple entry point for callers
    }
}
Facade simplifies the entry pointProxy handles the routing decision

100. Summarize, as a final decision checklist, when the Proxy pattern specifically is the correct choice over Decorator, Adapter, or Facade, and give the single question that most reliably distinguishes it from all three.

Reach for Proxy when the interface the client already depends on does not need to change, when the point of wrapping is controlling whether, when, or how a call reaches the real subject rather than adding stacked behavior, translating an incompatible contract, or simplifying a whole subsystem, and when the client must never be able to tell, through behavior, exceptions, or type-checking, that it's holding a stand-in rather than the real thing.

The single most reliable distinguishing question: does this wrapper ever legitimately decide not to call through to the real object, or to change when or how it does? If yes, that's Proxy's defining trait. If it always forwards and only adds independent, stackable behavior around the call, that's Decorator; if it exists because the interfaces don't match, that's Adapter; if it exists to hide a whole subsystem behind one simpler entry point, that's Facade.

Controls whether/when/how the call happensThe line separating it from Decorator/Adapter/Facade
No comments
Leave a Comment