Enhancing Behavior with Interception and the Decorator Pattern

A fundamental aspect of flexible software design is the ability to combine simple components to create complex behaviors without altering the core components themselves. Much like adding layers to a base ingredient to create a complex dish, we can wrap a service implementation with additional logic to enhance its capabilities. This technique, known as interception, allows us to insert behavior between a caller and a service, effectively modifying how the service behaves or interacts with the system, all without changing the source code of either party.

Interception relies heavily on the principles of loose coupling and the Liskov Substitution Principle. By programming to interfaces rather than concrete implementations, we can wrap an object that implements an interface with another object that implements the same interface. This wrapping object, or interceptor, delegates calls to the wrapped instance while executing its own logic before or after the delegation.

The Decorator Design Pattern

The Decorator pattern is the structural foundation of interception. It provides a mechanism to attach additional responsibilities to an object dynamically. The pattern involves a interface, a concrete implementation of that interface (the component), and a decorator that also implements the interface and holds a reference to the component.

Consider a simple messaging interface:

public interface IMessageService
{
    string SendMessage(string message);
}

A basic implementation might look like this:

public class SimpleMessageService : IMessageService
{
    public string SendMessage(string message)
    {
        return message;
    }
}

We can create a decorator that modifies the output without changing the original service. For instance, a decorator that converts the message to uppercase:

public class UppercaseMessageDecorator : IMessageService
{
    private readonly IMessageService _originalService;

    public UppercaseMessageDecorator(IMessageService originalService)
    {
        _originalService = originalService;
    }

    public string SendMessage(string message)
    {
        var processed = _originalService.SendMessage(message);
        return processed.ToUpperInvariant();
    }
}

Decorators can be nested. You could have a decorator that adds a timestamp, wrapped around the uppercase decorator, which is in turn wrapped around the simple service. This creates a pipeline where each layer adds a specific concern.

Addressing Cross-Cutting Concerns

One of the primary uses of interception is managing cross-cutting concerns. These are aspects of a system that affect multiple components, such as logging, security, caching, or error handling. Because they span across the applicasion, implementing them directly within business logic classes violates the Single Responsibility Principle and leads to code duplication.

Using decorators, we can isolate these concerns into their own classes. For example, instead of adding audit logging code to every data access method, we can create a single AuditingDecorator that wraps a repository.

Implementing an Auditing Decorator

Suppose we have an IOrderRepository interface. We want to audit every update operation without modifying the repository code. We can define an AuditingOrderRepositoryDecorator.

public class AuditingOrderRepositoryDecorator : IOrderRepository
{
    private readonly IAuditLogger _auditLogger;
    private readonly IOrderRepository _innerRepository;

    public AuditingOrderRepositoryDecorator(
        IAuditLogger auditLogger,
        IOrderRepository innerRepository)
    {
        _auditLogger = auditLogger;
        _innerRepository = innerRepository;
    }

    public void UpdateOrder(Order order)
    {
        _innerRepository.UpdateOrder(order);
        _auditLogger.LogAction("OrderUpdated", order.Id);
    }

    public Order GetById(Guid id)
    {
        // Read operations might not require auditing
        return _innerRepository.GetById(id);
    }
}

In this example, the decorator intercepts the call to UpdateOrder, performs the core operation via the inner repository, and then executes the auditing logic. The original repository remains completely unaware of the audit trail.

Handling Resilience with a Circuit Breaker

Applications that rely on remote resources (like web services or databases) must handle transient failures. The Circuit Breaker pattern prevents an application from repeatedly trying to execute an operation that is likely to fail, allowing the system to fail fast and preserve resources.

We can implement this behavior using a decorator. Assume we have a remote product service that is prone to timeouts. We can wrap it in a CircuitBreakerDecorator.

public class CircuitBreakerProductRepositoryDecorator : IProductRepository
{
    private readonly ICircuitBreakerState _stateManager;
    private readonly IProductRepository _repository;

    public CircuitBreakerProductRepositoryDecorator(
        ICircuitBreakerState stateManager,
        IProductRepository repository)
    {
        _stateManager = stateManager;
        _repository = repository;
    }

    public IEnumerable<product> GetAll()
    {
        _stateManager.Guard(); // Throws if circuit is open
        try
        {
            var result = _repository.GetAll();
            _stateManager.RecordSuccess();
            return result;
        }
        catch (Exception ex)
        {
            _stateManager.RecordFailure(ex);
            throw;
        }
    }
}
</product>

The Guard method checks the state of the circuit. If too many failures have occurred recently, the circuit is "open," and the guard throws an exception immediately, preventing the call to the remote service. If the call succeeds, we record a success, potential resetting the circuit. This adds a layer of resilience to the core repository logic without modifying the repository itself.

Centralized Error Handling

While a circuit breaker manages stability, we also need to communicate errors to the user appropriately. A UI component should not crash due to a network exception. Instead, we can use an error-handling decorator to catch specific exceptions and translate them into user-friendly messages.

public class ErrorHandlingProductRepositoryDecorator : IProductRepository
{
    private readonly IProductRepository _repository;
    private readonly IUserNotifier _notifier;

    public ErrorHandlingProductRepositoryDecorator(
        IProductRepository repository,
        IUserNotifier notifier)
    {
        _repository = repository;
        _notifier = notifier;
    }

    public void Insert(Product product)
    {
        try
        {
            _repository.Insert(product);
        }
        catch (NetworkException ex)
        {
            _notifier.NotifyUser("Connection failed. Please check your network.");
        }
        catch (SystemException ex)
        {
            _notifier.NotifyUser("An unexpected error occurred.");
        }
    }
}

This decorator absorbs technical exceptions and replaces them with user-facing notifications, ensuring the application remains stable and informative.

Enforcing Security Constraints

Security is another classic cross-cutting concern. We often need to ensure that only authorized users can perform specific actions, such as deleting records. A security decorator can check permissions before allowing the call to pass through.

public class SecureOrderRepositoryDecorator : IOrderRepository
{
    private readonly IUserContext _userContext;
    private readonly IOrderRepository _repository;

    public SecureOrderRepositoryDecorator(
        IUserContext userContext,
        IOrderRepository repository)
    {
        _userContext = userContext;
        _repository = repository;
    }

    public void DeleteOrder(Guid id)
    {
        if (!_userContext.IsInRole("Manager"))
        {
            throw new UnauthorizedAccessException("User does not have permission to delete orders.");
        }

        _repository.DeleteOrder(id);
    }

    public Order GetById(Guid id)
    {
        // Read access might be public
        return _repository.GetById(id);
    }
}

Here, the DeleteOrder method is intercepted. The decorator verifies that the current user belongs to the "Manager" role. If not, it blocks the call. The read operation, GetById, is passed through directly. This separation allows the security logic to evolve independently of the business logic in the repository.

Composition and Duplication

While the Decorator pattern is powerful, applying it manually across many interfaces can lead to repetitive code (boilerplate). Implementing a circuit breaker for IProductRepository, IOrderRepository, and ICustomerRepository involves writing similar wrapper code multiple times.

Although the decorator code is relatively simple, the repetition highlights a scaling issue. This limitation is often addressed by more advanced techniques such as dynamic interception (using libraries that generate decorators at runtime) or Aspect-Oriented Programming (AOP), which can apply these cross-cutting rules automatically via attributes or configuration.

Posted on Sun, 25 Oct 2026 16:46:10 +0000 by osusano