Java design pattern deep dive
Decorator Pattern in Java: 100 interview questions with professional answers.
Learn how to attach responsibilities to an object at runtime without touching its class, how java.io and the Servlet API use Decorator internally, and where wrapping chains go wrong in real production systems.
What makes a good Decorator answer?
Interviewers are rarely testing whether you can wrap one object. They want to see the shared-interface contract, full delegation, single responsibility per layer, and awareness of when wrapping order matters.
| Approach | Use when | Watch out for |
|---|---|---|
| Decorator | You need to add optional, combinable responsibilities to individual objects at runtime behind a shared interface. | Deep chains are harder to debug and can silently drop delegated calls if a layer is incomplete. |
| Inheritance per feature | There is exactly one fixed combination of behavior known at compile time. | Combinatorial explosion of subclasses once behaviors can mix and match. |
| Proxy | You need to control or defer access to an object (lazy loading, remote access, permission checks) without adding new behavior. | Overlaps structurally with Decorator; the intent, not the code shape, is what differs. |
| Strategy | You want to swap one interchangeable algorithm, not layer several additive behaviors. | Does not compose multiple concerns the way stacked decorators do. |
| AOP / interceptors | A framework can weave cross-cutting concerns declaratively across many classes. | Less explicit than a Decorator chain; harder to trace without framework tooling. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off worth naming out loud, and the production concern that separates a textbook answer from a professional one.
1. Explain the intent of the Decorator pattern and implement a Coffee interface with combinable Milk and Sugar decorators.
The Decorator pattern lets you attach new responsibilities to an individual object dynamically, without modifying its class or affecting sibling instances. It works by wrapping a component behind the same interface the client already depends on, so each layer can add behavior before or after delegating to the object it wraps.
public interface Coffee {
String getDescription();
double getCost();
}
public class PlainCoffee implements Coffee {
public String getDescription() { return "Coffee"; }
public double getCost() { return 2.00; }
}
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee wrapped;
protected CoffeeDecorator(Coffee wrapped) { this.wrapped = wrapped; }
}
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + " + milk"; }
public double getCost() { return wrapped.getCost() + 0.50; }
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + " + sugar"; }
public double getCost() { return wrapped.getCost() + 0.25; }
}
// usage
Coffee order = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
System.out.println(order.getDescription() + " = $" + order.getCost());
2. What are the four GoF structural roles, and how do they map onto Java's InputStream/FilterInputStream hierarchy?
The classic roles are Component (the common interface), ConcreteComponent (the base object being decorated), Decorator (an abstract wrapper implementing Component and holding a reference to another Component), and ConcreteDecorator (a specific added behavior). In java.io, InputStream is the Component, FileInputStream is the ConcreteComponent, FilterInputStream is the abstract Decorator, and classes like BufferedInputStream and GZIPInputStream are ConcreteDecorators.
InputStream in = new BufferedInputStream(
new GZIPInputStream(
new FileInputStream("data.gz")));
FilterInputStream holds a protected volatile InputStream in field and its default methods simply call through to it, which is exactly the Decorator base-class delegation pattern described in the GoF book.
3. Why must a decorator implement the same interface as the wrapped object rather than subclass the concrete component directly?
If a decorator subclassed the concrete component, it would inherit one fixed implementation and could only add behavior to that specific class, not to any other implementation of the interface or to another decorator. Implementing the shared interface instead means the decorator can wrap any object that satisfies the contract, including another decorator, which is what makes stacking arbitrary combinations possible.
It also preserves substitutability: client code that expects a Coffee never needs to know whether it received a PlainCoffee or three layers of decorators, because both satisfy the same type.
4. Implement a Notifier interface with EmailNotifier as the base and SMSDecorator/SlackDecorator stacked on top so all channels fire from one call.
Each decorator sends through its own channel and then delegates to the wrapped notifier so the call fans out across every layer in the chain.
public interface Notifier {
void send(String message);
}
public class EmailNotifier implements Notifier {
public void send(String message) { System.out.println("Email: " + message); }
}
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
protected NotifierDecorator(Notifier wrapped) { this.wrapped = wrapped; }
public void send(String message) { wrapped.send(message); }
}
public class SMSDecorator extends NotifierDecorator {
public SMSDecorator(Notifier wrapped) { super(wrapped); }
public void send(String message) {
super.send(message);
System.out.println("SMS: " + message);
}
}
public class SlackDecorator extends NotifierDecorator {
public SlackDecorator(Notifier wrapped) { super(wrapped); }
public void send(String message) {
super.send(message);
System.out.println("Slack: " + message);
}
}
// usage
Notifier notifier = new SlackDecorator(new SMSDecorator(new EmailNotifier()));
notifier.send("Deployment finished");
5. How does Decorator add responsibilities to one object at runtime without affecting other instances, and why is static inheritance unable to do that?
Because decoration happens by wrapping a specific object reference at runtime, only that reference gains the new behavior; every other instance of the same concrete class is untouched. Inheritance, by contrast, is resolved at compile time and applies to the whole class: if you create a subclass with extra behavior, every instance you construct from that subclass carries it, and you cannot selectively add or remove it from one already-existing object.
This is the core reason Decorator exists as a separate pattern from subclassing: it moves the decision of "does this object get this behavior" from class-definition time to object-construction time.
6. Describe how a Decorator base class holds a reference to the wrapped Component and delegates, using a TextView and BorderDecorator example.
An abstract decorator typically stores the wrapped component as a final field set in its constructor, and its default method bodies simply forward to that field. Concrete decorators then override only the methods they want to change, calling the stored reference before or after adding their own logic.
public interface TextView {
String render();
}
public class PlainTextView implements TextView {
private final String text;
public PlainTextView(String text) { this.text = text; }
public String render() { return text; }
}
public abstract class TextViewDecorator implements TextView {
protected final TextView wrapped;
protected TextViewDecorator(TextView wrapped) { this.wrapped = wrapped; }
public String render() { return wrapped.render(); }
}
public class BorderDecorator extends TextViewDecorator {
public BorderDecorator(TextView wrapped) { super(wrapped); }
public String render() { return "[ " + wrapped.render() + " ]"; }
}
7. What distinguishes structural decoration (wrapping behind the same interface) from simply calling helper methods before and after invoking the original object?
A helper method that runs code before and after a call is a local, one-off convenience: the caller still holds a direct reference to the original object and must remember to route every call through the helper. Structural decoration replaces the reference itself with a wrapper that satisfies the same interface, so every existing call site automatically gets the new behavior without modification, and wrappers can be composed, reordered, and reused across the codebase.
The key test is substitutability: if you can pass the wrapped object anywhere the original type is expected and nothing downstream needs to change, you have a true Decorator; if callers must know to invoke a special method, you just have a utility function.
8. How are Collections.unmodifiableList(), Collections.synchronizedList(), and Collections.checkedList() all Decorator applications on List?
Each of these factory methods returns a new object implementing List that wraps the original list and forwards most calls to it, while intercepting specific methods to add behavior: unmodifiableList throws on mutators, synchronizedList wraps every method in a lock, and checkedList validates element types on insertion. The original list is untouched and unaware it is being wrapped.
List base = new ArrayList<>();
List safe = Collections.synchronizedList(base);
List readOnly = Collections.unmodifiableList(safe);
// readOnly wraps safe, which wraps base — a two-layer decorator chain
9. Why is Decorator classified as a structural pattern, and how does that differ from a behavioral pattern like Strategy?
Structural patterns are concerned with how classes and objects are composed to form larger structures while keeping interfaces stable; Decorator solves the structural problem of attaching extra responsibilities to an object by composing it inside wrapper objects that share its interface. Strategy is behavioral: it solves the problem of varying an algorithm's behavior by swapping one interchangeable implementation for another, with no notion of layering or wrapping.
In practice this means Decorator answers "how do I add to what this object does" while Strategy answers "how do I choose which of several equivalent behaviors this object uses right now."
10. Build a Pizza ordering system where toppings are decorators, and explain how getDescription() and getPrice() recurse through the chain.
Every topping decorator calls the wrapped pizza's method first and then appends or adds its own contribution, so calling either method on the outermost decorator triggers a recursive walk all the way down to the base pizza and back up, accumulating description text and price at each layer.
public interface Pizza {
String getDescription();
double getPrice();
}
public class PlainPizza implements Pizza {
public String getDescription() { return "Pizza"; }
public double getPrice() { return 6.00; }
}
public abstract class ToppingDecorator implements Pizza {
protected final Pizza wrapped;
protected ToppingDecorator(Pizza wrapped) { this.wrapped = wrapped; }
}
public class CheeseTopping extends ToppingDecorator {
public CheeseTopping(Pizza wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + ", cheese"; }
public double getPrice() { return wrapped.getPrice() + 1.25; }
}
public class OliveTopping extends ToppingDecorator {
public OliveTopping(Pizza wrapped) { super(wrapped); }
public String getDescription() { return wrapped.getDescription() + ", olives"; }
public double getPrice() { return wrapped.getPrice() + 0.75; }
}
11. Trace exactly how a method call travels through three stacked decorators, including the order pre- and post-processing code runs in.
Calling a method on the outermost decorator runs that decorator's pre-processing code first, then it calls the same method on the object it wraps, which is the next decorator in, which runs its own pre-processing, and so on until the innermost concrete component executes. The result then bubbles back outward, with each decorator's post-processing code running in the reverse order: the last decorator to run pre-processing is the first to run post-processing.
// Outer(Middle(Inner(component))).operation() executes:
// Outer.pre -> Middle.pre -> Inner.pre -> component.operation()
// -> Inner.post -> Middle.post -> Outer.post
12. How does BufferedInputStream wrapping GZIPInputStream wrapping FileInputStream actually work, and why does wrapping order matter?
Each layer implements read() in terms of the stream it wraps: GZIPInputStream reads compressed bytes from the FileInputStream and inflates them, while BufferedInputStream reads larger chunks from whatever it wraps and serves single-byte or small reads out of an in-memory buffer to reduce system calls. Order matters because buffering must sit closest to the caller to absorb small reads efficiently, and decompression must sit between the buffer and the raw file so the bytes flowing through it are the compressed ones, not the inflated ones.
Reversing the order (buffering the raw file, then decompressing) still works correctly but buffers the wrong layer, missing the chance to reduce the number of small reads a caller makes against the decompressed stream.
13. Explain how the JVM resolves virtual method dispatch through a decorator chain, and why each layer adds one more indirect call.
Every decorator field is typed as the shared interface, so each delegating call is a virtual (interface) method invocation resolved through the JVM's dispatch tables at runtime rather than a direct call. Because each layer only knows the interface type of the object it holds, the JVM cannot resolve the concrete target until it walks the vtable/itable lookup for that specific object at each level, meaning an N-layer chain costs at least N indirect dispatches for a single logical operation.
The JIT can often inline a monomorphic call site (one where only one concrete type has ever been seen there), but a decorator field that could hold any implementation of the interface is more likely to become polymorphic or megamorphic, which limits how aggressively the JIT can optimize it (see Q43).
14. Implement a generic Decorator<T> abstract class using composition, and explain how constructor injection enforces the wrapping relationship.
A generic decorator base class parameterizes over the component type and requires the wrapped instance through its constructor, so it is structurally impossible to create a decorator instance without supplying something to wrap.
public abstract class Decorator {
protected final T wrapped;
protected Decorator(T wrapped) {
this.wrapped = Objects.requireNonNull(wrapped, "wrapped component required");
}
}
public abstract class CoffeeDecorator extends Decorator implements Coffee {
protected CoffeeDecorator(Coffee wrapped) { super(wrapped); }
}
Because the field is final and only assignable in the constructor, every concrete decorator subclass inherits the same guarantee without repeating the null check or the field declaration.
15. How would you implement a decorator that needs to call a method not on the shared interface, and what design change does that force?
If a decorator genuinely needs to invoke a method that isn't part of the common interface, you either widen the interface (adding the method for every implementation, which may not make sense for all of them), define a narrower additional interface that only the relevant components implement and cast to it defensively, or accept a more specific type in that decorator's constructor instead of the general interface.
public interface Coffee {
double getCost();
}
public interface Refillable {
void refill();
}
public class RefillDecorator extends CoffeeDecorator {
public RefillDecorator(Coffee wrapped) { super(wrapped); }
public void refillIfSupported() {
if (wrapped instanceof Refillable r) {
r.refill();
}
}
}
16. Describe how HttpServletRequestWrapper is structured as a Decorator, and how a filter chain layers multiple wrappers around one request.
HttpServletRequestWrapper implements HttpServletRequest and holds a reference to the request it wraps, delegating every method to it by default; a servlet filter can extend it and override only the methods it needs to change, such as getHeader() to inject or mask a value.
public class MaskingRequestWrapper extends HttpServletRequestWrapper {
public MaskingRequestWrapper(HttpServletRequest request) { super(request); }
@Override
public String getHeader(String name) {
if ("Authorization".equalsIgnoreCase(name)) return "***";
return super.getHeader(name);
}
}
Each filter in the chain can wrap the request it receives before calling chain.doFilter(wrappedRequest, response), so by the time the request reaches the servlet it may be several wrapper layers deep, exactly mirroring stacked Decorators around one component.
17. Trace what happens internally when you wrap a Decorator instance in another Decorator rather than wrapping a ConcreteComponent directly.
Since a Decorator implements the same interface as the component it wraps, it is itself a valid value to pass into another decorator's constructor; the outer decorator simply holds a reference typed to the interface and has no way to know, or need to know, that the object behind that reference is itself a wrapper. Calling a method on the outer decorator delegates to the inner decorator's implementation, which in turn delegates to whatever it wraps, continuing the chain until a genuine ConcreteComponent is reached.
Coffee c = new SugarDecorator(new MilkDecorator(new MilkDecorator(new PlainCoffee())));
// SugarDecorator wraps a MilkDecorator, which wraps another MilkDecorator,
// which wraps the PlainCoffee — three delegation hops for one getCost() call
18. How does Decorator interact with Java generics and bounded type parameters when a covariant return type must be preserved?
If a component's method returns a type parameter (for example, a Repository<T> whose save(T) returns T), the decorator must be generic over the same type parameter and forward it unchanged, since introducing a bound or a different type would break substitutability for callers relying on the exact return type.
public interface Repository {
T save(T entity);
}
public class LoggingRepositoryDecorator implements Repository {
private final Repository wrapped;
public LoggingRepositoryDecorator(Repository wrapped) { this.wrapped = wrapped; }
public T save(T entity) {
T result = wrapped.save(entity);
System.out.println("Saved: " + result);
return result;
}
}
Bounded wildcards (Repository<? extends T>) are rarely useful here because the decorator both receives and returns the same exact type, so an unbounded type parameter mirroring the component's is usually the cleanest fit.
19. How do Writer and FilterWriter propagate flush()/close() down a chain, and what happens if a decorator forgets to override close()?
FilterWriter declares flush() and close() as simple delegations to the wrapped writer, so a well-behaved subclass either relies on that default (if it has no resources of its own) or overrides it to flush/close its own state first and then call super.close() to propagate downward. If a custom decorator that buffers or holds a resource forgets to override close(), that resource is never released even though the underlying writer might still get closed correctly, because the inherited default only forwards the call rather than performing the subclass's own cleanup.
20. How can java.lang.reflect.Proxy implement decorator-like behavior without a hand-written wrapper class, and how does it differ from a real Decorator?
A dynamic proxy generates a class at runtime that implements a given set of interfaces and routes every method call through a single InvocationHandler, which can run code before and after delegating to the real target — functionally similar to decoration but implemented generically instead of with one class per behavior.
Coffee target = new PlainCoffee();
Coffee logged = (Coffee) Proxy.newProxyInstance(
Coffee.class.getClassLoader(),
new Class[]{ Coffee.class },
(proxy, method, args) -> {
System.out.println("Calling " + method.getName());
return method.invoke(target, args);
});
The key difference is that a hand-written Decorator is a distinct compiled class with its own named behavior and can hold typed state, while a dynamic proxy is generic infrastructure driven by a single handler, works only against interfaces, and trades compile-time clarity for flexibility and reduced boilerplate when many similar wrappers are needed (see also Q59).
21. What are the trade-offs between the Decorator pattern versus adding conditional flags or strategy fields to toggle optional behavior on one class?
Flags keep everything in one class, which is simpler to read for a small, fixed number of options, but every new flag multiplies the number of code paths inside that one class and makes it easy to introduce invalid flag combinations. Decorator moves each optional behavior into its own class that can be composed freely, keeping each piece simple and independently testable, at the cost of more classes and one extra virtual call per layer.
As a rule of thumb, two or three rarely-combined flags are fine as booleans; once behaviors need to combine in different orders or independently vary, Decorator scales better.
22. Discuss the trade-off between Decorator's flexibility and the difficulty of reading stack traces through a five-layer-deep decorator chain in production.
A stack trace from deep inside a five-layer decorator chain shows five delegating frames with generic names like CachingDecorator.get, RetryDecorator.get, and LoggingDecorator.get before it ever reaches the frame that actually failed, which obscures the real business logic location and makes on-call debugging slower under pressure.
23. Compare the trade-offs of implementing logging, caching, and retries via Decorator versus via a DI framework's AOP interceptors.
Hand-written decorators are explicit in code: you can see exactly which classes are wrapped and in what order just by reading the wiring, and there is no framework magic to understand, but you pay in boilerplate as the number of cross-cutting concerns and target interfaces grows. AOP interceptors (Spring AOP, AspectJ) let you apply a concern to many classes declaratively via pointcuts and annotations, drastically reducing boilerplate, but the actual call path is woven at runtime or build time and is less visible from reading a single class, which can make debugging and onboarding harder.
A common professional answer is to use Decorator for a small number of important, order-sensitive concerns, and AOP for concerns that need to apply broadly and uniformly across many unrelated classes.
24. Is Decorator worth using for a component with only two possible optional behaviors, or does class overhead outweigh the benefit versus simple parameterization?
For exactly two independent optional behaviors, a constructor flag, a builder option, or a `Set<Feature>` parameter is usually simpler and easier to read than introducing an abstract decorator base class plus two concrete decorators, since the combinatorial benefit of Decorator only pays off once the number of independently combinable behaviors grows or callers need to compose them dynamically at runtime from separately-sourced pieces.
Introduce Decorator when you anticipate more behaviors being added later, when the behaviors genuinely come from different modules or teams, or when the interface needs to remain open for extension without modification (the open/closed principle); otherwise the added classes are often not worth it for just two options.
25. What are the trade-offs of immutable decorators (returning a new wrapped instance) versus mutable decorators (adding/removing decorations on an existing wrapper)?
Immutable decoration, where every "add a behavior" operation returns a new wrapper around the existing chain, is thread-safe by construction, easy to reason about, and lets you freely share the original component across multiple differently-decorated variants without interference. Mutable decoration, where you can attach or detach behaviors on an existing object in place, avoids extra allocations and can be convenient for long-lived objects whose behavior needs to change over time, but it requires careful synchronization if shared across threads and makes it harder to reason about which behaviors are active at any given moment.
// immutable style
Coffee withMilk = new MilkDecorator(order); // order unchanged
Coffee withBoth = new SugarDecorator(withMilk); // withMilk unchanged
26. Discuss the trade-off between transparency (clients can't tell an object is decorated) and exposing decorator-specific methods that break it.
Full transparency means client code can treat a decorated object exactly like an undecorated one, which is the entire point of sharing the same interface; but sometimes a decorator's added capability (querying cache hit rate, forcing a retry count reset) is genuinely useful to expose, and doing so requires either an additional interface the client explicitly opts into or a downcast, both of which break the illusion that decoration is invisible.
The professional stance is to keep the common interface fully transparent for normal use, and expose decorator-specific operations only through a narrow, explicitly-typed extension interface that callers must deliberately request, rather than sprinkling public methods on the concrete decorator class that most callers will never see because they only hold the base interface type.
27. When does arbitrary decorator ordering become a liability, and how do you constrain valid orderings in a Java API?
Arbitrary ordering is a liability whenever layers are not commutative — for example, encrypting before compressing produces unusable output because encrypted data doesn't compress, while compressing before encrypting works correctly — so letting callers freely stack decorators in any order invites silent correctness bugs rather than compile errors.
public final class SecurePipeline {
public static OutputStream wrap(OutputStream raw) {
// enforced order: compress first, then encrypt
return new EncryptingOutputStream(new CompressingOutputStream(raw));
}
}
28. What is the trade-off between one configurable decorator with parameters versus several single-purpose decorators composed together?
A single configurable decorator (say, one LoggingDecorator with booleans for logging requests, responses, and timing) reduces the number of classes and avoids a deep chain, but it grows an internal conditional surface that mixes concerns and gets harder to test exhaustively as more options are added. Several single-purpose decorators keep each class trivially simple and independently testable, and let callers pick only what they need, at the cost of more classes and one extra delegation hop per concern.
Favor single-purpose decorators when behaviors are independently toggled or reused across different chains; favor one configurable decorator when the options are tightly coupled and always appear together.
29. Evaluate the trade-off between Decorator's per-object customization and the memory footprint of wrapping every instance in several small wrapper objects.
Each decoration layer is a small heap object holding at minimum a reference to the wrapped instance plus any of its own fields, so wrapping a large number of objects with multiple layers multiplies both allocation count and total memory versus a single flat object with equivalent fields; on top of the raw bytes, more objects means more work for the garbage collector to trace during a scan.
For a modest number of long-lived objects this overhead is negligible next to the design benefits, but for very high object counts (millions of decorated entries in a cache, for instance) it is worth measuring, and the Flyweight pattern is the classic complementary technique when per-object wrapper overhead actually becomes a bottleneck.
30. Discuss how Decorator trades compile-time type safety for runtime composability, and give an example where that causes a subtle bug.
Because any object implementing the shared interface can be passed into a decorator's constructor, the compiler cannot prevent nonsensical combinations that are technically type-correct but logically wrong, such as wrapping an already-encrypted stream with another encryption layer, or applying a currency-formatting decorator to a `PriceCalculator` twice.
Coffee order = new MilkDecorator(new MilkDecorator(new PlainCoffee()));
// compiles fine — silently double-charges for milk
The compiler only checks that the interface is satisfied, not that the combination or repetition makes domain sense, so guarding against this requires runtime checks, a builder that enforces valid combinations, or marker interfaces/annotations (see Q98) rather than relying on the type system alone.
31. Describe the 'decorator explosion' anti-pattern and how you would refactor it in Java.
Decorator explosion happens when so many independent optional behaviors accumulate that developers start creating pre-combined decorator classes for common cases (`MilkAndSugarDecorator`, `MilkSugarAndWhipDecorator`), defeating the purpose of composability and producing the same combinatorial class growth Decorator was meant to avoid.
CoffeeBuilder.of(base).withMilk().withSugar().build()), and resist the urge to hand-roll a class per combination just because a few combinations are common — cache or preset those combinations behind named factory methods instead of named classes.32. What happens when a decorator forgets to delegate a method call to the wrapped component, silently breaking part of the interface's contract?
If a concrete decorator overrides an interface method with a no-op or a default implementation instead of delegating (or simply forgets to override a method whose default doesn't delegate), calls to that method on the decorated object silently stop reaching the real component, which can look like a broken feature further downstream with no exception and no obvious cause.
public class LoggingList extends AbstractList {
private final List wrapped;
// BUG: extends AbstractList instead of delegating every method,
// so get(), size(), etc. throw UnsupportedOperationException
// unless every single one is manually overridden and forwarded
}
33. Why is adding new methods to a concrete decorator that aren't on the shared interface an anti-pattern, and how does it violate Liskov Substitution?
The Liskov Substitution Principle requires that any code using the base interface type continue to work correctly when given a decorated instance; if a decorator adds a public method not declared on that interface, code written against the base type simply can't call it, and code that downcasts to the concrete decorator type to call it becomes coupled to one specific decoration, breaking substitutability the moment the decoration is swapped, removed, or wrapped further.
If the extra capability is genuinely needed by callers, it belongs on a properly named interface the concrete decorator implements in addition to the base one, so callers who need it can depend on that narrower contract explicitly instead of on an undocumented concrete class.
34. What goes wrong when two decorators in the same chain both mutate shared state on the wrapped component instead of purely delegating?
Decorators are supposed to add behavior around calls, not reach into and mutate the wrapped component's internal state directly; if two decorators in the same chain both do this, their effects can interleave in an order-dependent and hard-to-predict way, and reordering the chain (which should be a safe, local change) can silently change program behavior because the mutations now happen in a different sequence relative to each other.
35. Why is double-wrapping with the same decorator type (e.g., LoggingDecorator(LoggingDecorator(service))) a mistake, and how would you detect it at runtime?
Wrapping the same concern twice produces duplicated side effects — a log line printed twice, a metric incremented twice, a retry policy applied on top of another retry policy multiplying the effective retry count — usually because of a wiring mistake in a DI configuration or a factory method being called more than once on an already-wrapped instance.
public abstract class Decorator {
protected final T wrapped;
protected Decorator(T wrapped) {
if (wrapped != null && getClass().isInstance(unwrapIfPossible(wrapped))) {
throw new IllegalArgumentException("Duplicate decoration detected: " + getClass());
}
this.wrapped = wrapped;
}
}
A lighter-weight detection strategy is to walk the chain at startup (each decorator exposing what it wraps) and assert no concrete decorator type appears twice, failing fast during application wiring rather than in production traffic.
36. What is the anti-pattern of a decorator silently swallowing or transforming exceptions from the wrapped component, and how does it hide incidents?
A decorator that catches an exception from the wrapped call and either discards it or converts it into a generic, lower-severity error (or worse, returns a default value as if the call succeeded) removes the caller's ability to detect and react to the real failure, which means monitoring, alerting, and retries downstream never see that anything went wrong.
public String get(String key) {
try {
return wrapped.get(key);
} catch (Exception e) {
return null; // BUG: caller can't tell "not found" from "backend is down"
}
}
37. Explain the mistake of implementing equals()/hashCode() on a decorator using only the wrapper's own fields, ignoring the wrapped component.
If a decorator's equals() compares only its own added fields and ignores the wrapped component, two decorators wrapping completely different underlying objects can be reported equal, and if it's used as a key in a HashMap or stored in a HashSet, this produces incorrect deduplication or lookup collisions where logically distinct decorated objects are treated as interchangeable.
The safer default is usually to delegate equals()/hashCode() to the wrapped component (so decoration doesn't change identity semantics at all) unless the decorator's own state is genuinely part of what makes two instances distinct, in which case it must be combined with, not substituted for, the wrapped component's identity.
38. Why is forgetting to override close()/flush() in a custom FilterOutputStream subclass dangerous, and what resource leak does it cause?
FilterOutputStream's inherited close() flushes and closes the wrapped stream, but if your subclass buffers bytes internally (for compression, encryption, or batching) and doesn't override close() to flush its own buffer first, those buffered bytes are simply lost even though the underlying file handle closes cleanly with no exception thrown.
public class BatchingOutputStream extends FilterOutputStream {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public BatchingOutputStream(OutputStream out) { super(out); }
@Override public void write(int b) { buffer.write(b); }
@Override public void close() throws IOException {
out.write(buffer.toByteArray()); // must flush buffered data first
super.close();
}
}
39. Describe the anti-pattern of using inheritance instead of Decorator for every combination of optional behavior.
If you model each combination of optional behaviors as its own subclass (`CoffeeWithMilk`, `CoffeeWithSugar`, `CoffeeWithMilkAndSugar`, `CoffeeWithMilkSugarAndWhip`), the number of classes grows combinatorially with the number of independent behaviors — n independent options require up to 2^n subclasses to cover every combination — which quickly becomes unmaintainable and duplicates logic across siblings.
Decorator solves exactly this by making each behavior an independently combinable wrapper, so n behaviors need only n decorator classes, and any combination is assembled at runtime rather than pre-declared as a class.
40. What mistake occurs when a decorator caches or memoizes state that should live on the wrapped component, causing stale results after the component changes?
If a decorator computes and caches a derived value (like a formatted description or a total price) at construction time or on first access, and the underlying component is mutable and changes afterward, the decorator keeps returning the stale cached value because it never re-reads the component, which is a subtle bug since the decorated object appears to work correctly until the wrapped state actually changes.
public class CachedDescriptionDecorator implements Coffee {
private final Coffee wrapped;
private final String cachedDescription; // BUG: computed once, never refreshed
public CachedDescriptionDecorator(Coffee wrapped) {
this.wrapped = wrapped;
this.cachedDescription = wrapped.getDescription();
}
public String getDescription() { return cachedDescription; }
}
Unless caching is the decorator's explicit, documented purpose (see Q64), a decorator should recompute from the live wrapped component on every call, or clearly document and bound the caching behavior it deliberately introduces.
41. What is the real runtime performance cost of several decorator layers around a hot-path method, and how would you measure it with JMH?
Each layer adds one virtual method dispatch plus whatever work that layer itself performs, which is typically a few nanoseconds of pure dispatch overhead per call for a well-optimized JIT-compiled path — negligible for most application code, but potentially measurable in tight loops executing millions of times per second.
@Benchmark
public double baseline(CoffeeState state) {
return state.plain.getCost();
}
@Benchmark
public double threeLayers(CoffeeState state) {
return state.sugarMilkMilkWrapped.getCost();
}
Use JMH with warmup iterations, @State(Scope.Benchmark) to avoid constant-folding by the JIT, and compare the two benchmarks' throughput or average time; in most realistic cases the difference is dwarfed by actual business logic, I/O, or allocation costs elsewhere in the call.
42. How does an unnecessary BufferedInputStream around an already-buffered stream hurt performance, and how would you catch it in review?
Wrapping a stream that is already internally buffered (or wrapping a `ByteArrayInputStream`, which reads from memory and gains nothing from buffering) with another `BufferedInputStream` adds an extra layer of array copying and bounds-checking on every read without reducing the number of underlying I/O operations, since there weren't excessive small reads to begin with.
43. Discuss how long decorator chains affect JIT inlining, and whether megamorphic call sites through decorators can cause deoptimization.
The JIT tracks how many distinct concrete types have been observed at each call site: a monomorphic site (always the same concrete type) can be inlined aggressively, a bimorphic site (two types) can still be handled with a type check plus two inlined branches, but a megamorphic site (many different types observed) falls back to a full virtual dispatch with no inlining, which is exactly the risk when a decorator field is typed to a widely-implemented interface and different call paths substitute different concrete decorators there.
This rarely causes outright deoptimization by itself, but it does mean the JIT can't specialize that call site the way it would for a stable, small set of implementations, so extremely polymorphic decorator usage in a hot loop should be profiled with an async-profiler or JFR flight recording rather than assumed to be free.
44. In a high-throughput logging pipeline, how do you measure whether a MetricsDecorator on every request handler adds unacceptable overhead?
Run the service under a representative load test with the metrics decorator enabled and disabled (a feature flag makes this easy to toggle), and compare p50/p99 latency and maximum sustainable throughput between the two runs; also profile CPU with async-profiler to see what fraction of samples land inside the decorator's own code (timer start/stop, histogram recording) versus the actual handler logic.
Timer.Sample sample = Timer.start(registry);
try {
return wrapped.handle(request);
} finally {
sample.stop(registry.timer("request.duration", "handler", name));
}
If overhead is unacceptable, batch metric recording (aggregate locally and flush periodically instead of recording per-call), use lock-free counters, or sample only a percentage of requests instead of instrumenting every single one.
45. Compare the memory overhead of wrapping ten thousand objects with three decorator layers each versus one class with three boolean flags.
Three decorator layers around ten thousand objects means up to forty thousand total objects on the heap (the base plus three wrappers each), each with its own object header (typically 12-16 bytes with compressed oops) plus a reference field, versus a single flat class per instance with three boolean fields packed into the same object — meaning the decorator approach uses meaningfully more memory at scale purely from object header and reference overhead, independent of what each layer actually does.
For ten thousand objects this difference (roughly a few hundred kilobytes) is rarely significant; it becomes worth addressing only at much larger scales, where Flyweight-style sharing of decorator instances (if the decorator itself is stateless) or switching to flags can measurably reduce heap pressure.
46. How would you design a caching decorator so it doesn't become a performance bottleneck under high concurrency, avoiding cache stampede?
A naive caching decorator that checks-then-computes-then-stores without coordination lets many concurrent threads all miss the cache for the same key simultaneously and all recompute the expensive value at once (a stampede); the fix is to use `ConcurrentHashMap.computeIfAbsent()` or a dedicated per-key lock so only one thread computes while others wait for that same computation to finish.
public class CachingDecorator implements PriceLookup {
private final PriceLookup wrapped;
private final ConcurrentMap cache = new ConcurrentHashMap<>();
public double lookup(String sku) {
return cache.computeIfAbsent(sku, wrapped::lookup);
}
}
47. Explain how excessive object allocation from decorator chains increases GC pressure in a latency-sensitive service, and how to mitigate it.
If decorators are constructed fresh on every request rather than once at startup and reused (for example, building a new `LoggingDecorator(new RetryDecorator(new MetricsDecorator(service)))` inside a hot request-handling method instead of at wiring time), every request allocates several short-lived wrapper objects, increasing young-generation garbage collection frequency and, under enough load, contributing to pause-time variance.
48. When would you avoid Decorator in a performance-critical path and instead inline the extra behavior directly into the concrete class?
In an extremely hot, latency-sensitive inner loop — a per-tick game engine update, a per-packet network processing routine, or a numerical kernel called billions of times — the extra indirection and allocation from even one decorator layer can be measurable, and inlining the behavior directly into the concrete class (accepting the loss of composability) can be the right trade-off once profiling actually confirms the decorator is the bottleneck.
This should be a measured decision, not a default assumption: most application-level code (HTTP handlers, service layers, repository calls) has I/O or business logic costs that dwarf decorator dispatch overhead by orders of magnitude, so premature inlining for imagined performance reasons usually costs more in maintainability than it ever saves in CPU time.
49. How does decorating a Comparator or Iterator in a tight loop affect throughput, and how would you refactor it?
Wrapping a `Comparator` with a logging decorator and using it inside `Collections.sort()` or a `PriorityQueue` means every single comparison during the sort pays the decorator's overhead (a log call, a counter increment) multiplied by O(n log n) comparisons, which for large collections can dominate the actual sort cost and, worse, can flood logs with one line per comparison.
// BAD: logs on every comparison during a sort of n elements
Comparator- logged = (a, b) -> {
log.debug("Comparing {} and {}", a, b);
return a.getPrice().compareTo(b.getPrice());
};
Refactor by moving the cross-cutting concern outside the loop entirely — log once before and after the sort with aggregate information (element count, duration) — rather than decorating the per-element operation itself.
50. Compare the performance of java.lang.reflect.Proxy-based dynamic decorators versus hand-written static decorators in a request-handling hot path.
A hand-written static decorator compiles to a normal class with direct field access and a regular (though virtual) method call, which the JIT can optimize like any other Java code; a `java.lang.reflect.Proxy` instance routes every call through `InvocationHandler.invoke()`, which involves boxing primitive arguments into an `Object[]`, a reflective `Method.invoke()` call to reach the real target, and generally cannot be inlined or optimized as aggressively as direct virtual dispatch.
For most services this difference is small enough to ignore, but in a very high-throughput request path, benchmark both with JMH before choosing dynamic proxies purely for their reduced boilerplate; CGLIB/Byte Buddy-generated subclass proxies (see Q59) typically perform closer to hand-written code than JDK dynamic proxies do, because they generate real bytecode rather than routing through reflection.
51. Compare Decorator with Proxy: both wrap an object behind the same interface, so what determines which pattern's intent actually applies?
Structurally, Decorator and Proxy look nearly identical — both implement the same interface as the object they wrap and hold a reference to it — but their intent differs: Decorator exists to add new responsibilities to an object, and is designed for open-ended stacking of multiple layers; Proxy exists to control access to an object (lazy initialization, remote invocation, permission checks, reference counting), and typically wraps exactly one target for one specific access-control purpose rather than being freely composable.
| Aspect | Decorator | Proxy |
|---|---|---|
| Purpose | Add behavior | Control access |
| Typical stacking | Multiple layers, freely combined | Usually one layer, one purpose |
| Example | BufferedInputStream | Hibernate lazy-loading entity proxy |
52. How does Decorator differ from Chain of Responsibility, given both involve a sequence of objects each handling part of a request?
In Decorator, every layer in the chain always participates and delegates to the next, contributing to a single combined result (the request always reaches the innermost component and every wrapper's contribution is additive). In Chain of Responsibility, each handler decides independently whether to process the request itself and stop, or pass it along unchanged to the next handler — the chain is about finding the one handler responsible for a request, not layering behavior that all applies together.
A useful distinguishing question: does every layer always run and combine its effect (Decorator), or does exactly one handler typically "claim" the request and the rest are skipped (Chain of Responsibility)?
53. Compare implementing cross-cutting behavior with Decorator versus Spring AOP @Aspect interceptors — what do you gain and lose with each?
Decorator gives you explicit, statically-typed, IDE-navigable wiring: you can see exactly which classes are wrapped, in what order, by reading the composition root, and there's no framework proxy machinery to understand when debugging. Spring AOP lets you apply a single `@Aspect` with a pointcut expression across dozens of unrelated beans without touching their code or wiring, dramatically reducing repetition, but the actual call path is generated by CGLIB/JDK proxies at runtime, which can surprise newcomers (self-invocation not being intercepted, final methods not being advised) and is less discoverable by reading code alone.
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logCall(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
log.info("{} took {}ns", pjp.getSignature(), System.nanoTime() - start);
}
}
}
54. Explain the difference between Decorator and Strategy when both could vary a PriceCalculator's behavior at runtime.
Strategy replaces the entire pricing algorithm with one of several interchangeable, mutually exclusive implementations chosen based on context (percentage discount, flat discount, no discount) — you pick exactly one. Decorator instead layers additive adjustments on top of a base calculation, where multiple layers can apply together and each contributes its own piece (base price, then a loyalty discount decorator, then a seasonal-promotion decorator, then a tax decorator).
The tell is whether the behaviors are mutually exclusive alternatives (Strategy) or independently combinable additions that stack (Decorator); some designs legitimately use both together, selecting a Strategy for the core algorithm and wrapping the result in Decorators for cross-cutting adjustments.
55. Compare Decorator with Composite: how do they complement each other, and how would a UI toolkit combine both for a tree of decorated widgets?
Composite lets you build a tree of objects (containers holding children, which may themselves be containers) and treat individual objects and groups uniformly through a shared interface; Decorator lets you wrap any single object in that tree with additional behavior without changing its type. A UI toolkit commonly combines both: a `Panel` (Composite) contains `Button` and `TextField` children, and any individual widget, including the panel itself, can be wrapped in a `ScrollDecorator` or `BorderDecorator` since both the composite and the decorator implement the same `Component` interface.
Component view = new BorderDecorator(
new ScrollDecorator(
new Panel(List.of(button, textField))));
56. When would you choose Decorator over functional composition with Function.andThen() to layer behavior onto a lambda?
Function.andThen() is a clean, lightweight way to compose pure transformations when there's exactly one method to layer and no meaningful shared state or lifecycle to manage; Decorator is the better fit when the component's interface has multiple methods that must all delegate consistently, when the decorator needs to hold its own state (a counter, a cache, a connection), or when the behavior needs a name and identity in the type system that functional composition doesn't provide.
Function pipeline = ((Function) x -> x + 1)
.andThen(x -> x * 2)
.andThen(x -> x - 3);
For a single-method functional interface, andThen/compose is often simpler than writing a full Decorator hierarchy; for a multi-method interface like Coffee or Repository, Decorator is the natural fit.
57. Compare Decorator versus subclass-per-feature inheritance for adding optional validation, logging, and caching to a Repository implementation.
Subclass-per-feature would require a `ValidatingLoggingCachingRepository`, a `LoggingCachingRepository`, a `ValidatingCachingRepository`, and so on for every combination actually needed, duplicating logic across siblings and making it hard to add a fourth optional feature without touching every existing subclass. Decorator lets you write `ValidatingRepositoryDecorator`, `LoggingRepositoryDecorator`, and `CachingRepositoryDecorator` once each, and compose exactly the combination each deployment needs at wiring time, with no combinatorial class growth.
Repository repo = new CachingRepositoryDecorator<>(
new LoggingRepositoryDecorator<>(
new ValidatingRepositoryDecorator<>(baseRepository)));
58. How does Decorator compare to Template Method when both customize parts of an algorithm's behavior?
Template Method fixes the overall algorithm's structure in a base class and lets subclasses override specific steps by overriding protected hook methods — customization happens through inheritance and is baked in at compile time per subclass. Decorator customizes behavior by wrapping the whole object from the outside at runtime, without requiring the component's own class hierarchy to anticipate the customization points at all.
Template Method is the right tool when the algorithm's skeleton is fixed and you're varying specific, known steps within one class family; Decorator is the right tool when you want to add behavior around an entire operation without the component needing to expose any customization hooks for it.
59. Compare a hand-rolled Decorator hierarchy with using CGLIB or Byte Buddy to generate decorating proxies dynamically — complexity vs flexibility.
Hand-rolled decorators are simple to understand, debug, and step through, but require writing (and maintaining) one class per behavior per target interface, which doesn't scale well if you need to apply the same small set of concerns across dozens of unrelated interfaces. CGLIB or Byte Buddy can generate a subclassing proxy at runtime that intercepts method calls generically, letting you apply one interception strategy across many types without writing per-type wrapper classes, including for concrete classes without interfaces (unlike JDK dynamic proxies).
Object proxy = new ByteBuddy()
.subclass(PaymentService.class)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(new LoggingInterceptor()))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.getDeclaredConstructor().newInstance();
The trade-off is debuggability and transparency: generated bytecode is harder to step through and reason about than a plain Java class, so this approach pays off mainly when the number of target types is large enough that hand-writing wrappers is genuinely impractical.
60. When is Facade a better fit than Decorator for simplifying a complex subsystem, versus when you actually need to add behavior transparently?
Facade's job is to provide a simpler, higher-level interface in front of a complex subsystem with many classes and interactions — it doesn't need to preserve the subsystem's original interface, and clients explicitly opt into using the simplified one. Decorator's job is to add behavior while preserving the exact same interface transparently, so existing clients continue to work unmodified.
If your goal is "make this easier to use by hiding complexity," reach for Facade; if your goal is "make this do one more thing without anyone needing to know," reach for Decorator — they solve different problems even though both sit in front of another object.
61. How would you unit test a CompressingOutputStream decorator in isolation, without needing a real file system or network resource?
Wrap a `ByteArrayOutputStream` instead of a real file or socket stream, since it fully implements the `OutputStream` contract in memory, then write known bytes through the decorator, close it, and assert on the captured byte array — either by decompressing it back and comparing to the original input, or by comparing against a known-good compressed byte sequence.
@Test
void compressesAndCanBeDecompressed() throws IOException {
ByteArrayOutputStream sink = new ByteArrayOutputStream();
try (OutputStream compressing = new CompressingOutputStream(sink)) {
compressing.write("hello world".getBytes(StandardCharsets.UTF_8));
}
byte[] decompressed = decompress(sink.toByteArray());
assertEquals("hello world", new String(decompressed, StandardCharsets.UTF_8));
}
62. Describe a strategy for testing that RetryDecorator wrapping CircuitBreakerDecorator wrapping a PaymentGateway calls the wrapped component in order under failure.
Use a mock or test double for the innermost `PaymentGateway` that can be programmed to fail a specific number of times before succeeding (or fail permanently), then assert both on the number of invocations reaching the mock and on the sequence of state transitions the circuit breaker reports, verifying the retry decorator waits for and reacts to the circuit breaker's outcome rather than bypassing it.
PaymentGateway flaky = mock(PaymentGateway.class);
when(flaky.charge(any()))
.thenThrow(new TimeoutException())
.thenThrow(new TimeoutException())
.thenReturn(Receipt.of("ok"));
PaymentGateway resilient = new RetryDecorator(new CircuitBreakerDecorator(flaky), 3);
Receipt result = resilient.charge(request);
verify(flaky, times(3)).charge(request);
assertEquals("ok", result.status());
63. How would you use Mockito to verify a LoggingDecorator delegates every call to the underlying mock component exactly once, with correct arguments?
Create a mock of the component interface, wrap it in the decorator, invoke the decorated method with specific arguments, and use `verify(mock, times(1)).method(argThat/eq(...))` to confirm the call reached the mock unchanged and exactly once — this also catches bugs where a decorator accidentally calls the wrapped method twice or transforms the arguments before forwarding.
@Test
void delegatesExactlyOnceWithSameArguments() {
Coffee mockCoffee = mock(Coffee.class);
when(mockCoffee.getCost()).thenReturn(2.00);
Coffee decorated = new LoggingDecorator(mockCoffee);
decorated.getCost();
verify(mockCoffee, times(1)).getCost();
verifyNoMoreInteractions(mockCoffee);
}
64. What edge cases should a test suite cover for a CachingDecorator around a PriceLookupService, including invalidation and concurrency?
Beyond the happy path (a repeated lookup for the same key returns the cached value without calling the wrapped service again), the suite should cover: cache miss on first access, explicit invalidation removing an entry so the next lookup re-queries the source, TTL expiry returning stale-then-fresh values correctly, concurrent lookups for the same uncached key not triggering duplicate underlying calls (no stampede), and behavior when the wrapped service itself throws (should the exception be cached, or retried on the next call?).
- Cache hit avoids calling the wrapped service
- Cache miss calls through and stores the result
- Invalidated/expired entries are recomputed, not served stale
- Concurrent access for the same key does not duplicate work
- Exceptions from the wrapped service are not silently cached as success
65. How would you write a test asserting a decorator preserves the exact exception type and message from the wrapped component, rather than wrapping it?
Configure the mocked or stubbed wrapped component to throw a specific exception type with a specific message, invoke the decorated method, and assert both the exact exception class (using `assertThrows` and checking `getClass()`, not just catching a supertype) and the message text, ensuring the decorator neither swallows it nor re-wraps it in a different exception type that would break callers catching the original type.
@Test
void propagatesOriginalExceptionUnchanged() {
PaymentGateway failing = mock(PaymentGateway.class);
when(failing.charge(any())).thenThrow(new InsufficientFundsException("balance too low"));
PaymentGateway decorated = new LoggingDecorator(failing);
InsufficientFundsException ex = assertThrows(InsufficientFundsException.class,
() -> decorated.charge(request));
assertEquals("balance too low", ex.getMessage());
}
66. Explain how you would test that combining decorators in a different order (Sugar(Milk(coffee)) vs Milk(Sugar(coffee))) produces the expected output difference.
Construct both orderings against the same base component, call the same method on each, and assert on the specific difference the order should produce — for a coffee example the price is the same either way (addition is commutative) but the description text differs; for a compress-then-encrypt example, one order produces valid output and the other produces garbage, which the test should explicitly assert.
@Test
void orderAffectsDescriptionButNotCost() {
Coffee sugarThenMilk = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
Coffee milkThenSugar = new MilkDecorator(new SugarDecorator(new PlainCoffee()));
assertEquals("Coffee + milk + sugar", sugarThenMilk.getDescription());
assertEquals("Coffee + sugar + milk", milkThenSugar.getDescription());
assertEquals(sugarThenMilk.getCost(), milkThenSugar.getCost(), 0.001);
}
67. How would you design a contract test suite that runs against the undecorated component and every decorated variant to guarantee transparency?
Write the contract assertions once as a parameterized or shared test class that only calls methods on the shared interface type, then supply it with a list of factory suppliers producing the raw component and each decorated variant (and combinations); running the identical assertions against every variant catches any decorator that breaks the interface's contract (see Q32) rather than relying on separate, potentially inconsistent test classes per decorator.
@ParameterizedTest
@MethodSource("coffeeVariants")
void costIsAlwaysNonNegative(Coffee coffee) {
assertTrue(coffee.getCost() >= 0);
}
static Stream coffeeVariants() {
Coffee base = new PlainCoffee();
return Stream.of(base, new MilkDecorator(base), new SugarDecorator(new MilkDecorator(base)));
}
68. What is the best way to test that a decorator propagates close()/shutdown() calls, including verifying no leak when an exception occurs mid-chain?
Use a fake or spy `Closeable` resource that records whether `close()` was called, wrap it in the decorator chain, and assert `close()` was invoked exactly once even when a method call earlier in a try-with-resources block throws — this specifically tests that exception paths still trigger cleanup, not just the happy path.
@Test
void closesWrappedResourceEvenWhenOperationThrows() {
Closeable spyResource = mock(Closeable.class);
Service decorated = new RetryDecorator(new FailingService(spyResource));
assertThrows(RuntimeException.class, () -> {
try (decorated) {
decorated.doWork();
}
});
verify(spyResource, times(1)).close();
}
69. How would you test thread-safety of a decorator that adds synchronization around a non-thread-safe wrapped component?
Drive many concurrent threads through the decorated component performing operations that would corrupt shared state if unsynchronized (like incrementing a shared counter or appending to a shared list inside the wrapped component), then assert the final state is exactly what's expected given the total number of operations, which fails intermittently or deterministically if synchronization is missing or incorrect.
ExecutorService pool = Executors.newFixedThreadPool(16);
SynchronizedCounterDecorator counter = new SynchronizedCounterDecorator(new PlainCounter());
List> tasks = IntStream.range(0, 10_000)
.mapToObj(i -> pool.submit(counter::increment))
.toList();
for (Future t : tasks) t.get();
assertEquals(10_000, counter.value());
Tools like the `jcstress` framework or repeated stress runs under `-XX:+UseParallelGC` with high thread counts help surface races that a single quick test run might miss.
70. How would you use property-based testing to verify any valid combination and ordering of decorators around a Shape interface never violates area/perimeter invariants?
Generate random sequences of available decorators (border padding, scaling, rotation) applied in random order and random count to a randomly generated base shape, then assert invariants that must hold regardless of combination — for example, area and perimeter must always be non-negative and finite, and a scaling-only decorator chain's total area must equal the base area times the product of all scale factors squared, independent of the order those scale decorators were applied in.
@Property
void areaIsNeverNegativeForAnyDecoratorCombination(
@ForAll("shapes") Shape base,
@ForAll("decoratorSequences") List> decorators) {
Shape result = decorators.stream().reduce(base, (s, d) -> d.apply(s), (a,b) -> b);
assertTrue(result.area() >= 0);
assertTrue(Double.isFinite(result.perimeter()));
}
Property-based tools like jqwik generate many random combinations automatically, which is far more likely to surface an ordering-dependent bug than a handful of hand-picked example tests.
71. Design a decorator-based solution for adding request/response logging, gzip compression, and API-key auth to an existing HttpClient-based service client.
Define a small `RequestExecutor` interface the existing client already effectively implements, then layer one decorator per concern so the business logic in the original client stays completely untouched: an `AuthDecorator` adds the API key header, a `CompressionDecorator` sets `Content-Encoding: gzip` and compresses the body, and a `LoggingDecorator` records request/response metadata, composed in an order where auth headers are added last so compression doesn't interfere with header inspection.
RequestExecutor client = new LoggingDecorator(
new AuthDecorator(
new CompressionDecorator(rawHttpClientExecutor),
apiKeySupplier));
72. A RetryDecorator wrapping a PaymentGateway retried non-idempotent calls after timeouts, causing duplicate charges — diagnose and redesign it safely.
The root cause is that a timeout doesn't tell you whether the downstream operation actually completed — the payment may have succeeded and only the response was lost — so blindly retrying a non-idempotent `charge()` call risks charging the customer twice. The safe redesign requires idempotency: generate a client-side idempotency key per logical charge attempt and pass it through so the payment gateway (or an idempotency layer in front of it) can recognize and deduplicate a retried request with the same key, returning the original result instead of processing it again.
public Receipt charge(ChargeRequest request) {
String idempotencyKey = request.idempotencyKey(); // stable across retries
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return wrapped.charge(request, idempotencyKey);
} catch (TimeoutException e) {
if (attempt == maxAttempts) throw e;
}
}
throw new IllegalStateException("unreachable");
}
73. Design a decorator-based solution for per-tenant rate limiting on a multi-tenant REST API's service layer without modifying each service implementation.
Wrap each tenant-facing service interface with a `RateLimitingDecorator` that resolves the current tenant from request context, looks up or lazily creates a per-tenant token bucket, and either proceeds or rejects with a `RateLimitExceededException` before delegating to the wrapped service — since the decorator only depends on the shared service interface and tenant context, it applies uniformly without any individual service needing rate-limiting logic of its own.
public class RateLimitingDecorator implements InvocationHandler {
private final T target;
private final Map limiters = new ConcurrentHashMap<>();
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String tenant = TenantContext.current();
RateLimiter limiter = limiters.computeIfAbsent(tenant, t -> RateLimiter.create(100.0));
if (!limiter.tryAcquire()) throw new RateLimitExceededException(tenant);
return method.invoke(target, args);
}
}
74. Explain how Servlet Filters wrapping HttpServletRequest/Response are a real-world Decorator chain, and a pitfall from wrapping in the wrong order.
Each `Filter.doFilter()` can wrap the request or response it received in a new wrapper before passing it to `chain.doFilter()`, and the servlet at the end of the chain sees whatever composition of wrappers was built up — exactly the Decorator pattern applied to the servlet request/response objects. The pitfall is that if a compression filter wraps the response stream before an authentication filter has a chance to reject the request and write an error body, the error response can end up compressed when the client isn't expecting it, or a filter reading the request body early can prevent a later filter (or the servlet itself) from reading it again since input streams aren't typically re-readable.
75. How would you add OpenTelemetry tracing spans around every outbound call using a Decorator around RestTemplate/WebClient, without instrumenting each call site?
Wrap the `RestTemplate`/`WebClient` (or, more precisely, its underlying `ClientHttpRequestInterceptor` or `ExchangeFilterFunction` extension point, which is itself a decorator hook) so that every outbound call automatically starts a span, injects trace-context headers for propagation, and ends the span with the response status when the call completes — all without any call site needing to know tracing exists.
WebClient client = WebClient.builder()
.filter((request, next) -> {
Span span = tracer.spanBuilder(request.url().toString()).startSpan();
try (Scope scope = span.makeCurrent()) {
ClientRequest traced = ClientRequest.from(request)
.headers(h -> propagator.inject(Context.current(), h, HttpHeaders::set))
.build();
return next.exchange(traced)
.doOnNext(resp -> span.setAttribute("http.status_code", resp.statusCode().value()))
.doFinally(sig -> span.end());
}
})
.build();
Post a Comment
Add