Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Crosscutting Concerns Explained

Definition

A crosscutting concern is functionality that affects multiple parts of an application but doesn’t naturally fit into a single module or component.

Examples of Crosscutting Concerns

ConcernWhere it appliesWhy it’s crosscutting
LoggingEvery functionNeeded across all modules
Performance monitoringCritical pathsScattered across components
CachingExpensive operationsApplied inconsistently
AuthorizationPublic APIsDuplicated in every endpoint
TransactionsDatabase operationsRepeated in every DAO
Retry logicNetwork callsSpread across HTTP, database, etc.
Metrics collectionKey operationsManually added everywhere
ValidationInput handlingCopy-pasted validation code

The Core Problem

Horizontal vs Vertical Concerns

Traditional modularity handles vertical concerns well:

#![allow(unused)]
fn main() {
mod user {
    pub fn create_user(...) { }
    pub fn delete_user(...) { }
}

mod order {
    pub fn create_order(...) { }
    pub fn cancel_order(...) { }
}

mod payment {
    pub fn process_payment(...) { }
    pub fn refund_payment(...) { }
}
}

Each module focuses on one domain (users, orders, payments).

But horizontal concerns cut across all modules:

                 Logging
                    ↓
┌─────────────────────────────────┐
│  user::create_user()            │ ← Needs logging
│  user::delete_user()            │ ← Needs logging
├─────────────────────────────────┤
│  order::create_order()          │ ← Needs logging
│  order::cancel_order()          │ ← Needs logging
├─────────────────────────────────┤
│  payment::process_payment()     │ ← Needs logging
│  payment::refund_payment()      │ ← Needs logging
└─────────────────────────────────┘

Logging, metrics, and caching are needed in all modules, breaking encapsulation.

Code Scattering

Without AOP, crosscutting code is scattered across your codebase:

#![allow(unused)]
fn main() {
// user.rs
fn create_user(name: String) -> Result<User, Error> {
    log::info!("Creating user: {}", name);
    let start = Instant::now();

    let user = database::insert_user(name)?;

    log::info!("User created in {:?}", start.elapsed());
    metrics::record("user_created", start.elapsed());
    Ok(user)
}

// order.rs
fn create_order(items: Vec<Item>) -> Result<Order, Error> {
    log::info!("Creating order with {} items", items.len());
    let start = Instant::now();

    let order = database::insert_order(items)?;

    log::info!("Order created in {:?}", start.elapsed());
    metrics::record("order_created", start.elapsed());
    Ok(order)
}

// payment.rs
fn process_payment(amount: u64) -> Result<Receipt, Error> {
    log::info!("Processing payment: ${}", amount);
    let start = Instant::now();

    let receipt = payment_gateway::charge(amount)?;

    log::info!("Payment processed in {:?}", start.elapsed());
    metrics::record("payment_processed", start.elapsed());
    Ok(receipt)
}
}

Problem: The same logging/timing/metrics pattern is copy-pasted three times!

Code Tangling

Crosscutting code tangles with business logic:

#![allow(unused)]
fn main() {
fn transfer_funds(from: Account, to: Account, amount: u64) -> Result<(), Error> {
    // Logging (line 1-2)
    log::info!("Transferring ${} from {} to {}", amount, from.id, to.id);
    let start = Instant::now();

    // Authorization (line 4-7)
    if !has_permission("transfer", from.user_id) {
        log::error!("Unauthorized transfer attempt");
        return Err(Error::Unauthorized);
    }

    // Validation (line 9-12)
    if amount == 0 || amount > from.balance {
        log::error!("Invalid transfer amount");
        return Err(Error::InvalidAmount);
    }

    // Business logic (finally! line 14-17)
    from.balance -= amount;
    to.balance += amount;
    database::save_account(from)?;
    database::save_account(to)?;

    // Metrics (line 19-21)
    log::info!("Transfer completed in {:?}", start.elapsed());
    metrics::record("transfer", start.elapsed());

    Ok(())
}
}

Business logic (lines 14-17) is buried in 20+ lines of crosscutting code!

Maintenance Nightmare

Scenario: Update Log Format

Boss: “Add correlation IDs to all logs for distributed tracing.”

Without AOP:

  • Find all log::info! calls (100+ locations)
  • Manually update each one
  • Hope you didn’t miss any
  • Test everything again
#![allow(unused)]
fn main() {
// Before
log::info!("Creating user: {}", name);

// After
log::info!("[correlation_id={}] Creating user: {}", get_correlation_id(), name);
}

With aspect-rs:

  • Update LoggingAspect::before() (1 location)
  • Recompile
  • Done!
#![allow(unused)]
fn main() {
impl Aspect for LoggingAspect {
    fn before(&self, ctx: &JoinPoint) {
        log::info!("[{}] → {}", get_correlation_id(), ctx.function_name);
    }
}
}

Testing Difficulty

Crosscutting code makes unit testing harder:

#![allow(unused)]
fn main() {
#[test]
fn test_transfer_funds() {
    // Must mock logging
    let _log_guard = setup_test_logging();

    // Must mock metrics
    let _metrics_guard = setup_test_metrics();

    // Must mock authorization
    let _auth_guard = setup_test_auth();

    // Finally test business logic
    let result = transfer_funds(...);
    assert!(result.is_ok());
}
}

With aspect-rs:

#![allow(unused)]
fn main() {
#[test]
fn test_transfer_funds() {
    // Test pure business logic, no mocking needed
    let result = transfer_funds_impl(...);
    assert!(result.is_ok());
}
}

The AOP Solution

AOP lets you modularize crosscutting concerns:

#![allow(unused)]
fn main() {
// Define once
struct LoggingAspect;
impl Aspect for LoggingAspect {
    fn before(&self, ctx: &JoinPoint) {
        log::info!("[{}] → {}", get_correlation_id(), ctx.function_name);
    }
}

// Apply everywhere
#[aspect(LoggingAspect::new())]
fn create_user(name: String) -> Result<User, Error> { ... }

#[aspect(LoggingAspect::new())]
fn create_order(items: Vec<Item>) -> Result<Order, Error> { ... }

#[aspect(LoggingAspect::new())]
fn process_payment(amount: u64) -> Result<Receipt, Error> { ... }
}

Benefits:

  • No scattering: Logging logic in one place
  • No tangling: Business logic stands alone
  • Easy maintenance: Change logging in LoggingAspect
  • Better testing: Test business logic without mocks

Next, let’s learn the AOP Terminology used throughout aspect-rs.