Decorator for Logging and Caching in Services
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
A service decorator implements the same interface, receives another implementation, and adds behavior around delegation. For example, a metrics decorator can time findOrder, call the wrapped service in a try block, and record success or failure without changing the service implementation. Decorators can be stacked, but order matters: logging outside a cache records hits, while logging inside records only misses that reach the service. A caching wrapper also needs consistency, expiry, and concurrency policies. Spring's @Cacheable and @Transactional features use AOP proxies and interceptors; they are related wrapping techniques, but their proxy boundaries, such as self-invocation, must be understood instead of calling them literal decorators.
Decorator for Logging and Caching in Services
Direct Answer
A service decorator implements the service contract, holds another implementation, and adds behavior around delegation. Logging, metrics, validation, or caching can fit when the wrapper preserves the contract and its order is deliberate. Spring AOP features use proxies and interceptors, which are related wrapper techniques but are more accurately described as Proxy/AOP than as literal hand-written Decorator.
Code Example
interface OrderService {
Order findById(Long id);
void save(Order order);
}
@Service
class OrderServiceImpl implements OrderService {
public Order findById(Long id) {
// Business logic: fetch from database
return new Order(id, "Laptop", 999.99);
}
public void save(Order order) {
// Business logic: persist to database
}
}
class LoggingOrderService implements OrderService {
private final OrderService wrapped;
LoggingOrderService(OrderService service) {
this.wrapped = service;
}
public Order findById(Long id) {
System.out.println("LOG: findById(" + id + ") called"); // Add logging
var start = System.currentTimeMillis();
var result = wrapped.findById(id); // Delegate to wrapped
System.out.println("LOG: findById took " + (System.currentTimeMillis() - start) + "ms");
return result;
}
public void save(Order order) {
System.out.println("LOG: save(" + order + ") called");
wrapped.save(order);
}
}
class CachingOrderService implements OrderService {
private final OrderService wrapped;
private final Map<Long, Order> cache = new ConcurrentHashMap<>();
CachingOrderService(OrderService service) {
this.wrapped = service;
}
public Order findById(Long id) {
return cache.computeIfAbsent(id, wrapped::findById); // Cache or delegate
}
public void save(Order order) {
wrapped.save(order);
cache.put(order.id(), order); // Update cache after save
}
}
// Usage: stack decorators
OrderService service = new LoggingOrderService(
new CachingOrderService(
new OrderServiceImpl()
)
);
Common Mistakes & Pitfalls
- Adding concerns directly in service: Freshers put logging statements inside business methods, mixing infrastructure with domain logic—this violates Single Responsibility Principle and makes the service harder to test and maintain.
- Forgetting to implement the interface: Creating a decorator that extends the concrete class instead of implementing the interface breaks substitutability and couples the decorator to one implementation.
- Not delegating all methods: Implementing only the decorated methods and forgetting others causes runtime errors when clients call unimplemented methods—use an abstract base decorator that delegates everything by default.
- Overusing decorators: Adding decorators for every tiny concern creates deep wrapping chains that are hard to debug—sometimes a simple method call or aspect-oriented programming is cleaner.
Check your understanding
Answer these without looking back at the article.
- Why does @Transactional not work on self-calls in Spring?
- How does Spring AOP differ from manual Decorator pattern?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.