ADR-005 AOP
ADR-005: AOP Implementation via Method Interception¶
Status: Accepted
Context¶
Applications often require cross-cutting concerns (logging, tracing, caching, security, transactions) applied across multiple methods. Manually adding this logic pollutes business code and violates the DRY (Don't Repeat Yourself) principle. We needed a non-intrusive way to add behavior around method calls.
Decision¶
We implemented Aspect-Oriented Programming (AOP) using method interception:
- MethodInterceptor Protocol: Defined an interface with an invoke(ctx: MethodCtx, call_next) method. Implementations of this protocol contain the cross-cutting logic.
- @intercepted_by(InterceptorType, ...) Decorator: Applied to component methods to specify which interceptors should wrap them. Interceptors themselves must be registered components.
- UnifiedComponentProxy: An internal dynamic proxy. When a component with intercepted methods is resolved, it's wrapped in this proxy.
- getattr Hook: The proxy's getattr intercepts calls to decorated methods. It resolves the required interceptor instances from the container and builds an execution chain (dispatch_method) that calls each interceptor's invoke method around the original method call.
- Async Awareness: The proxy and dispatch_method were designed to correctly handle await for async def intercepted methods and async def invoke methods.
Consequences¶
Positive: - Cleanly separates cross-cutting concerns from business logic. - Interceptors are reusable and testable components. - The @intercepted_by decorator is explicit and declarative. - Supports both sync and async methods seamlessly. - Avoids complex bytecode manipulation, relying only on standard Python features (proxies, decorators).
Negative: - Adds a layer of indirection (the proxy), which can slightly complicate debugging if not understood. - Minor performance overhead due to proxying and interceptor chain execution on each call to an intercepted method (though caching of the wrapped method helps). - Relies on dynamic proxying, which might interact unexpectedly with tools that do heavy introspection.
Alternatives Considered¶
- Metaclass-based weaving: Rejected to avoid metaclass constraints and complexity when combining with existing class hierarchies.
- Bytecode weaving/instrumentation: Rejected due to fragility, tooling complexity, and reduced debuggability.
- Manual decorators per concern: Rejected as it spreads concern-specific code across many call sites and becomes error-prone.
- Monkey-patching at runtime: Rejected due to maintenance risks and lack of type-safety and clarity.
Notes¶
- Interceptor execution order follows the order declared in @intercepted_by.
- Interceptors must be registered/resolvable in the container to be applied.
- Wrapped methods are cached per instance to reduce repeated proxy-chain construction overhead.