Aspect-Oriented Programming
Separating cross-cutting concerns from core business logic.
Overview
Aspect-Oriented Programming (AOP) addresses cross-cutting concerns: functionality like logging, security checks, caching, and transaction management that cuts across multiple modules. AOP separates these concerns into "aspects" that are woven into the program at defined join points. This keeps business logic clean and avoids duplicating infrastructure code throughout the codebase.
Origin
Gregor Kiczales and colleagues at Xerox PARC published the foundational AOP paper in 1997. AspectJ (1998) was the first widely-used AOP language extension for Java. Spring Framework adopted AOP for transaction management and security in its 2003 release, making it a mainstream enterprise pattern.
Examples
Method interception via decorators in TypeScript
function Log(target: object, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const start = performance.now();
console.log('[' + key + '] called with', JSON.stringify(args));
try {
const result = await original.apply(this, args);
const ms = (performance.now() - start).toFixed(2);
console.log('[' + key + '] completed in ' + ms + 'ms');
return result;
} catch (err) {
console.error('[' + key + '] failed:', err);
throw err;
}
};
return descriptor;
}
class OrderService {
@Log
async placeOrder(userId: number, items: string[]): Promise<string> {
// business logic only; no logging code here
return 'order-' + Date.now();
}
}TypeScript decorators (stage 3 proposal, enabled with experimentalDecorators or using the new TC39 decorator spec) weave logging around the method without modifying the method body. The business logic is untouched.
Around advice pattern in Ruby using prepend
module TransactionAspect
def save(record)
DB.transaction do
result = super
AuditLog.record(action: 'save', record_id: record.id, user: Current.user)
result
end
end
end
module CacheAspect
def find(id)
cache_key = "record:#{id}"
cached = Redis.current.get(cache_key)
return JSON.parse(cached) if cached
result = super
Redis.current.setex(cache_key, 300, result.to_json)
result
end
end
class RecordRepository
prepend TransactionAspect
prepend CacheAspect
def save(record) = DB[:records].insert(record.attributes)
def find(id) = DB[:records].where(id: id).first
endRuby prepend inserts the module before the class in the method lookup chain, so calling super reaches the original method. This is how ActiveRecord callbacks and Mongoid middleware are implemented.
Use Cases
- 01Transaction demarcation in Spring or Rails where @Transactional / ActiveRecord callbacks wrap persistence logic without cluttering service methods
- 02Security enforcement: Spring Security's @PreAuthorize intercepts method calls before execution to verify permissions
- 03Performance instrumentation: timing and metrics collection applied uniformly across all service methods without modifying each one
- 04Request/response logging middleware in Rack, Express, or ASP.NET Core where every HTTP call is intercepted at the framework level
When Not to Use
- //When the cross-cutting behaviour needs to vary per-call based on arguments; aspects apply uniformly and become awkward when conditional weaving is needed
- //Debugging complex systems where aspects make the actual execution path non-obvious from the source code alone
- //Small projects where a direct function call to a logger or transaction helper is clearer than an implicit weaving mechanism
Technical Notes
- AspectJ supports compile-time, post-compile, and load-time weaving. Spring AOP uses runtime proxies (JDK dynamic proxies for interfaces; CGLIB subclasses for classes), which cannot intercept self-calls within the same object
- Ruby's Module#prepend (introduced in Ruby 2.0) is the idiomatic AOP mechanism. include appends to the ancestor chain; prepend inserts before. The distinction matters for super call routing
- TypeScript legacy decorators (experimentalDecorators) and the new TC39 Stage 3 decorator spec differ in how they access the descriptor; migrating between them requires code changes
- Pointcut expressions in AspectJ use a rich language: execution(* com.example.service.*.*(..)) matches all methods in the service package. Overly broad pointcuts are a common source of unintended interception
More in Types of Programming