Your business logic is beautiful. It’s clean, expressive, and solves exactly what the user needs. Then, reality hits. You need to add logging. You need to verify permissions. You need to manage database transactions. You need to handle retries.
Suddenly, your five-line ProcessOrder method is buried under twenty lines of "noise." This is the problem of Cross-Cutting Concerns—functionalities that affect multiple layers of an application but don't belong to the core business logic of any single module.
Enter Aspect-Oriented Programming (AOP).
AOP is a programming paradigm that aims to increase modularity by allowing the separation of these concerns. Instead of scattering logging code everywhere, you define it in one place (an Aspect) and tell the framework where to apply it (a Pointcut).
In this guide, we will explore how AOP is implemented in three of the most popular ecosystems today: TypeScript, C#, and Java. While they look similar on the surface, their underlying "engines" are fundamentally different.
Before we dive into code, we need to align on the four horsemen of AOP:
LoggingAspect).Service namespace").In the TypeScript ecosystem, AOP is primarily achieved through Decorators. Unlike Java or C#, TypeScript decorators are active functions. They are executed when the class is defined, allowing you to literally wrap the original method with a new one.
Here is how you would implement a @Log decorator that works across TypeScript, but we'll focus on the modern Method Decorator.
// The Aspect (Decorator Function)
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// The Advice (Around)
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Calling ${propertyKey} with:`, JSON.stringify(args));
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG] ${propertyKey} returned:`, JSON.stringify(result));
return result;
} catch (error) {
console.error(`[LOG] ${propertyKey} failed with:`, error);
throw error;
}
};
}
class OrderService {
@Log // Applying the Aspect
process(orderId: string, amount: number) {
// Pure Business Logic
return { success: true, id: orderId };
}
}
Why it works: TypeScript's compiler replaces the process method with the function returned by the decorator. It’s a direct, runtime function-wrapping mechanism.
Java, specifically via Spring AOP, takes a different approach. It uses Annotations as passive markers and Dynamic Proxies to orchestrate the logic. When you call a method on a Spring Bean, you are often calling a "wrapper" object created by Spring.
// 1. The Marker (Annotation)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {}
// 2. The Aspect
@Aspect
@Component
public class LoggingAspect {
// The Pointcut & Advice (Around)
@Around("@annotation(Loggable)")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[LOG] Calling " + methodName + " with: " + Arrays.toString(args));
try {
Object result = joinPoint.proceed(); // Execute original method
System.out.println("[LOG] " + methodName + " returned: " + result);
return result;
} catch (Exception e) {
System.err.println("[LOG] " + methodName + " failed: " + e.getMessage());
throw e;
}
}
}
// 3. The Target
@Service
public class OrderService {
@Loggable // Marker detected by Spring
public OrderResult process(String orderId, double amount) {
return new OrderResult(true, orderId);
}
}
Why it works: Spring detects the @Aspect and the @Loggable annotation. At runtime, it generates a proxy for OrderService. When process() is called, the proxy runs the LoggingAspect code before and after the real method.
C# attributes are passive metadata. By default, they do nothing. To achieve AOP in .NET, you historically needed a "weaver" (like PostSharp) that modified the IL code. However, modern .NET (8+) and frameworks like Castle DynamicProxy or Source Generators allow for cleaner interception.
// 1. The Marker (Attribute)
[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute { }
// 2. The Aspect (Interceptor)
public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var methodName = invocation.Method.Name;
var arguments = string.Join(", ", invocation.Arguments);
Console.WriteLine($"[LOG] Calling {methodName} with: {arguments}");
try
{
invocation.Proceed(); // Execute original method
Console.WriteLine($"[LOG] {methodName} returned: {invocation.ReturnValue}");
}
catch (Exception ex)
{
Console.WriteLine($"[LOG] {methodName} failed: {ex.Message}");
throw;
}
}
}
// 3. The Target
public class OrderService
{
[Log] // Metadata marker
public virtual OrderResult Process(string orderId, decimal amount)
{
return new OrderResult { Success = true, Id = orderId };
}
}
Why it works: Unlike Java which does this almost automatically in Spring, in C# you usually register the interceptor in your Dependency Injection (DI) container (using libraries like Autofac or Castle.Core). The virtual keyword is often required so the proxy can override the method.
| Dimension | TypeScript | Java (Spring) | C# (.NET) |
|---|---|---|---|
| Mechanical Nature | Active Interception: The decorator is a function that replaces the original method at load time. | Runtime Proxy: A wrapper object is created around the real object by the framework. | Metadata + Interceptor: Attributes store data; a proxy or IL weaver injects the logic. |
| Complexity | Low. Built into the language (with experimental flags or stage 3 proposal). | Medium. Requires the Spring IoC container to manage the objects. | High. Requires DI configuration and often third-party proxy libraries. |
| Performance | Minimal overhead. It's just a function call. | Slight overhead due to the proxy and reflection-based metadata lookup. | Varies. IL Weaving (PostSharp) is fastest; Reflection-based proxies have overhead. |
| Requirement | Decorator support enabled. | Must be a managed Bean (injected by Spring). | Method must often be virtual (for proxying) or use IL weaving. |
AOP is a double-edged sword. It's incredibly powerful but can make debugging a nightmare if overused.
AOP is about separation of concerns. By moving "how we log" or "how we authorize" out of the "what we do," we create cleaner, more maintainable systems.
The choice of tool depends on your stack, but the goal remains the same: let your business logic breathe.

Software Architect
Pós-graduado em arquitetura de software e soluções. Conecto profundidade técnica com resultados de negócio para entregar produtos que as pessoas realmente usam. Também mentoro desenvolvedores e criadores em programas ao vivo, podcasts e iniciativas de comunidade focadas em tecnologia inclusiva.
Continúa explorando temas similares

Durante anos, o JavaScript foi tratado como uma solução “boa o suficiente” no backend. Ele funcionava, mas não oferecia garantias estruturais comparáveis a linguagens tradicionais de servidor como Java ou C#.

A CVE-2025-55182, conhecida como React2Shell, revelou uma falha crítica em React Server Components e Next.js que permite RCE não autenticado e já está sendo explorada em produção — muitas vezes com PoCs gerados por IA. Neste artigo, mostro o que aconteceu, como saber se seu app está vulnerável, o que fazer agora e por que incidentes assim vão ficar cada vez mais comuns na era da inteligência artificial.

Nos últimos meses, os agentes de Inteligência Artificial deixaram de ser apenas copilotos de código e passaram a agir como verdadeiros engenheiros virtuais. Ferramentas como Claude Code, Codex CLI e Gemini CLI estão mudando completamente a forma como desenvolvedores escrevem, testam e otimizam código.
Una lista de 47 puntos para encontrar errores, riesgos de seguridad y problemas de rendimiento antes del lanzamiento.
Templates probados en producción, usados por desarrolladores. Ahorra semanas de setup en tu próximo proyecto.
