Skip to content

AOP Overview

Aspect-Oriented Programming (AOP) is a paradigm that separates cross-cutting concerns — code that spans multiple modules (logging, authentication, metrics) — from business logic.

The Problem: Cross-Cutting Concerns

Without AOP, repeating concerns appear everywhere:

fun processPayment(amount: Double, userId: String) {
    logger.info("processPayment called")     // logging
    checkAuthenticated(userId)               // auth
    metrics.increment("payment.calls")       // metrics
    // --- actual business logic ---
    chargeCard(amount, userId)
}

fun refundPayment(paymentId: String) {
    logger.info("refundPayment called")      // logging (again)
    checkAuthenticated(userId)               // auth (again)
    metrics.increment("refund.calls")        // metrics (again)
    // --- actual business logic ---
    reverseCharge(paymentId)
}

The AspectK Solution

With AspectK, cross-cutting concerns become centralized aspects:

// Business logic — clean
@Logged @Authenticated @Metered
fun processPayment(amount: Double, userId: String) {
    chargeCard(amount, userId)
}

@Logged @Authenticated @Metered
fun refundPayment(paymentId: String) {
    reverseCharge(paymentId)
}

// Cross-cutting concern — centralized
@Aspect
object LoggingAspect {
    @Before(target = [Logged::class])
    fun log(jp: JoinPoint) = logger.info("${jp.signature.methodName} called")
}

Design Philosophy

Why not AspectJ?

AspectJ is the most powerful AOP framework for JVM, but its pointcut expression language introduces significant complexity:

// AspectJ — string-based pointcut expression
@Aspect
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..)) && " +
            "@annotation(com.example.Logged) && " +
            "within(com.example..*)")
    public void log(JoinPoint jp) { ... }
}

Problems with this approach:

  • String-based, not type-safe: Pointcut expressions are plain strings — typos compile fine, errors only appear at runtime.
  • IDE support is limited: Refactoring a class or method name won't update the pointcut string.
  • Complex syntax: The AspectJ expression language (execution(), within(), args(), etc.) has a steep learning curve.
  • JVM only: AspectJ is tied to JVM bytecode manipulation and cannot target Kotlin Multiplatform.

AspectK: Type-Safe by Design

AspectK uses KClass references instead of string literals, making pointcuts fully type-safe:

// AspectK — type-safe, no strings, no runtime container
@Aspect
object LoggingAspect {
    @Before(target = [Logged::class])  // KClass reference, not a string
    fun log(jp: JoinPoint) { ... }
}
AspectJ AspectK
Pointcut definition String expression KClass reference
Type-safe? ❌ No ✅ Yes
Refactor-safe? ❌ No ✅ Yes
Runtime dependency ❌ No (weave-time) ❌ No
KMP support ❌ JVM only ✅ All targets

With AspectK, the target parameter takes KClass<out Annotation> references. If you rename or delete an annotation class, the compiler immediately flags every aspect that references it — no runtime surprises.


How Weaving Works: Before vs After Compilation

AspectK uses Kotlin's K2 IR (Intermediate Representation) transformation API. After your source code is parsed and type-checked, the compiler plugin intercepts the IR tree and injects advice calls directly into the function body.

Before Compilation (your source code)

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class Logged

@Aspect
object LoggingAspect {
    @Before(target = [Logged::class])
    fun log(joinPoint: JoinPoint) {
        println("→ ${joinPoint.signature.methodName}(${joinPoint.args.joinToString()})")
    }
}

class OrderService {
    @Logged
    fun placeOrder(orderId: String, amount: Double) {
        println("Order placed: $orderId, $amount")
    }
}

After IR Transformation (equivalent generated code)

The compiler plugin rewrites placeOrder to inject the advice call at the very top of the function body, before any original statement:

// This is NOT code you write — it shows what the compiler generates internally.
// Identifiers containing `$` are backtick-quoted in Kotlin; AspectK generates these names.

class OrderService {
    // Generated by AspectK: synthetic inner object holding compile-time metadata,
    // initialized once per class load (not per call).
    private object `$MethodSignatures` {
        val `ajc$tjp_0`: MethodSignature = MethodSignature(
            methodName = "placeOrder",
            annotations = listOf(
                AnnotationInfo(Logged::class, "com.example.Logged", emptyList(), emptyList()),
            ),
            parameter = listOf(
                // The dispatch receiver (this) is always the first entry
                MethodParameter("<this>", OrderService::class, "com.example.OrderService", emptyList(), false),
                MethodParameter("orderId", String::class, "kotlin.String", emptyList(), false),
                MethodParameter("amount",  Double::class, "kotlin.Double", emptyList(), false),
            ),
            returnType = Unit::class,
            returnTypeName = "kotlin.Unit",
        )
    }

    fun placeOrder(orderId: String, amount: Double) {
        // ↓ Injected by AspectK at compile time
        LoggingAspect.log(
            DefaultJoinPoint(
                target    = this,
                signature = `$MethodSignatures`.`ajc$tjp_0`,
                // args[0] is always the dispatch receiver (this) for member functions,
                // followed by the declared value parameters in order
                args      = listOf(this, orderId, amount),
            )
        )
        // ↓ Original function body — untouched
        println("Order placed: $orderId, $amount")
    }
}

Key points

  • The advice call is the very first statement in the transformed function body. No wrappers, no proxy objects — just a direct function call.
  • MethodSignature is initialized once per class load, not once per invocation. AspectK generates a synthetic inner object $MethodSignatures inside the containing class, with one property per intercepted function (ajc$tjp_0, ajc$tjp_1, …).
  • JoinPoint.args always starts with the dispatch receiver (this) for member functions, followed by the declared value parameters in declaration order. For top-level functions there is no dispatch receiver, so args contains only the value parameters and target is null.
  • MethodSignature.parameter mirrors JoinPoint.args — the first entry is the <this> receiver parameter for member functions, followed by the declared value parameters.
  • The original function body is preserved exactly. AspectK only prepends advice calls; it does not rewrite or wrap the original code.
  • No runtime framework required. The generated IR is pure Kotlin bytecode — the same as if you had written the call manually.

AspectK vs AspectJ: Full Comparison

Feature AspectK AspectJ
Language Kotlin Java / Kotlin
Weaving Compile-time (K2 IR) Compile-time (bytecode) / Load-time
Pointcut style Annotation KClass reference String expression language
Type-safe pointcuts ✅ Yes ❌ No
Refactor-safe ✅ Yes (compiler errors) ❌ No (silent breakage)
KMP support ✅ Full (JVM, JS, WASM, Native) ❌ JVM only
Runtime overhead ❌ None ❌ None (compile-time weaving)
Reflection at runtime ❌ No ❌ No
Advice types @Before @Before, @After, @Around, etc.
Learning curve Low (plain Kotlin) High (expression language)

Key Terminology

Term Meaning in AspectK
Aspect A class/object annotated with @Aspect
Advice A method annotated with @Before inside an aspect
Join Point A function call site where advice is injected
Pointcut An annotation class referenced in @Before(target = [...])
Weaving The K2 IR transformation that injects advice calls at compile time