Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.aidonow.com/llms.txt

Use this file to discover all available pages before exploring further.

Executive Summary

Repository boilerplate for event-sourced domain entities follows a structurally uniform pattern that is amenable to procedural macro code generation. Across seven domain entities, manual repository implementations totaled 4,977 lines; after macro-driven refactoring, the equivalent functionality required 275 lines of declaration—a 94% reduction. The macro infrastructure itself comprises 2,917 lines of implementation code written once and applied uniformly across all entities. Verifier analysis identified three defects in the initial implementation—missing multi-item pattern support, absent GSI query generation, and undefined cache invalidation semantics—all prior to production deployment. A critical security defect (missing tenant isolation validation in generated save() methods) was identified through manual cargo expand inspection and remediated before deployment. The net maintenance benefit is substantial: bug fixes propagate from one macro change to all consuming entities simultaneously, rather than requiring manual application across each entity’s repository implementation.

Key Findings

  • Procedural macros provide a 94% line count reduction for structurally uniform repository patterns. The investment in macro infrastructure (2,917 lines) reaches break-even at approximately four entities and yields compounding returns for all subsequent entities.
  • AI agents excel at pattern extraction and implementation; they require human guidance for abstraction boundary design. Pattern analysis across existing implementations was produced by an AI evaluator with high fidelity; the decision of which abstractions to expose as macro attributes required human judgment.
  • Generated code must be explicitly inspected for security-critical invariants. A missing tenant isolation check in generated save() methods was not caught by automated testing and required manual cargo expand review. AI-generated code does not inherit security awareness from surrounding code patterns.
  • Macro error messages require developer-experience design that AI does not apply by default. Initial macro error messages were technically accurate but non-actionable; human revision was required to produce messages that specified the problem, the fix, and a correct usage example.
  • Incremental migration outperforms bulk refactoring. Migrating one entity, verifying thoroughly, and applying lessons to the macro before proceeding to the next entity surfaced defects that a simultaneous multi-entity migration would have embedded undetected.
  • Macro changes are amplified changes. A single modification to a macro affects every entity using that macro simultaneously; versioning, deprecation, and backward-compatibility discipline are required for macro APIs from first use.

1. Introduction: The Boilerplate Problem

Event-sourced domain entities in a multi-tenant SaaS platform require a consistent set of surrounding infrastructure: event replay and versioning logic, repository implementations for test (in-memory) and production (DynamoDB) environments, a caching decorator, and event accessor helpers. Across seven CRM domain entities (Account, Contact, Lead, Opportunity, Activity, Product, Address), this infrastructure totaled 4,977 lines of near-identical code. The practical costs of this volume are not merely aesthetic. When a defect is found in the DynamoDB save() method pattern, it must be identified and corrected in seven repository implementations independently. When a development agent produces a new entity, it must replicate the pattern without deviation across all repository types. These costs compound as entity count grows. The following section documents a representative manual implementation to establish the baseline:
// 1. Event sourcing boilerplate (~150 lines)
impl Account {
    pub fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Created(e) => { /* apply logic */ }
            AccountEvent::Updated(e) => { /* apply logic */ }
            // ... 10-15 event variants
        }
    }

    pub fn replay(events: Vec<AccountEvent>) -> Self {
        let mut account = Self::default();
        for event in events {
            account.apply(&event);
        }
        account
    }

    pub fn uncommitted_events(&self) -> &[AccountEvent] {
        &self.events
    }

    pub fn mark_events_committed(&mut self) {
        self.events.clear();
    }
}

// 2. Repository boilerplate (~200 lines)
pub struct InMemoryAccountRepository {
    storage: Arc<RwLock<HashMap<AccountId, Account>>>,
}

impl InMemoryAccountRepository {
    pub async fn save(&self, account: Account) -> Result<()> {
        let mut storage = self.storage.write().await;
        storage.insert(account.id, account);
        Ok(())
    }

    pub async fn get(&self, id: AccountId) -> Result<Option<Account>> {
        let storage = self.storage.read().await;
        Ok(storage.get(&id).cloned())
    }

    // ... 8-10 more CRUD methods
}

// 3. DynamoDB repository boilerplate (~250 lines)
pub struct DynamoDbAccountRepository {
    client: CapsuleClient,
    table_name: String,
}

impl DynamoDbAccountRepository {
    pub async fn save(&self, account: Account) -> Result<()> {
        let entity = AccountEntity::from_domain(account);
        let item = to_item(&entity)?;

        self.client
            .client()
            .put_item()
            .table_name(&self.table_name)
            .set_item(Some(item))
            .send()
            .await?;

        Ok(())
    }

    pub async fn get(&self, id: AccountId) -> Result<Option<Account>> {
        let key = AccountEntity::primary_key(id);
        let result = self.client
            .client()
            .get_item()
            .table_name(&self.table_name)
            .set_key(Some(key))
            .send()
            .await?;

        match result.item {
            Some(item) => {
                let entity: AccountEntity = from_item(item)?;
                Ok(Some(entity.to_domain()))
            }
            None => Ok(None)
        }
    }

    // ... 8-10 more CRUD methods
}

// 4. Caching repository wrapper (~150 lines)
pub struct CachedAccountRepository<R> {
    inner: R,
    cache: Cache<AccountId, Account>,
}

// ... more boilerplate
The Hidden Cost: Copy-paste programming creates maintenance debt that scales with entity count.When I found a bug in the DynamoDB save method, I had to manually fix it in 7 places. When I missed one, it caused a production data corruption issue.

2. Macro Design

2.1 Pattern Analysis

An AI evaluator agent was provided access to all seven existing repository implementations with the directive to identify common patterns and recommend a macro abstraction strategy. The agent’s output identified five structurally distinct patterns:
  1. Event sourcing infrastructure — version tracking, uncommitted event accumulation, replay from event sequence
  2. Event accessor helpers — aggregate ID access, event type string representation, metadata methods
  3. In-memory repository — HashMap-backed CRUD with Arc<RwLock<>> concurrency
  4. DynamoDB repositoryput_item / get_item with entity-domain conversion and tenant isolation
  5. Caching decorator — Moka-based read-through cache with write-invalidate semantics
The agent correctly identified that differences between entity implementations (Account versus Contact versus Lead) were parameter values, not structural differences—enabling a single parameterized macro per pattern rather than entity-specific implementations.

2.2 Design Decision: Explicit Over Inferential

Two macro design approaches were evaluated: Option A (Inference-heavy): The macro auto-detects field names, event types, and access patterns from struct field inspection, requiring minimal annotations. Option B (Explicit annotation): All configuration is expressed through explicit attributes; the macro fails with a diagnostic compile error if required annotations are absent. Option B was selected. Inference-based macros fail at runtime with opaque errors when field names change or naming conventions differ from assumed defaults. Explicit annotation macros fail at compile time with actionable messages. For security-critical patterns such as tenant isolation, the preference for compile-time failures over runtime surprises is not merely stylistic—it has direct operational safety consequences.

3. Implementation

3.1 The Five Macros

The macro implementation comprised 2,917 lines across five procedural macros: DomainAggregate — generates event replay, uncommitted event tracking, version management, and test fixture construction. Eliminates approximately 150 lines per entity. Usage:
#[derive(DomainAggregate)]
#[id_type = "AccountId"]
#[event = "AccountEvent"]
pub struct Account {
    pub id: AccountId,
    pub name: String,
    pub industry: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}
The macro generates:
impl Account {
    // Event sourcing methods
    pub fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Created(e) => {
                self.id = e.account_id;
                self.name = e.name.clone();
            }
            AccountEvent::Updated(e) => {
                if let Some(name) = &e.name {
                    self.name = name.clone();
                }
            }
        }
        self.version += 1;
        self.events.push(event.clone());
    }

    pub fn replay(events: Vec<AccountEvent>) -> Self {
        let mut aggregate = Self::default();
        for event in events {
            aggregate.apply(&event);
        }
        aggregate.mark_events_committed();
        aggregate
    }

    pub fn uncommitted_events(&self) -> &[AccountEvent] {
        &self.events
    }

    pub fn mark_events_committed(&mut self) {
        self.events.clear();
    }

    pub fn test_fixture() -> Self {
        Self {
            id: AccountId::new_v4(),
            name: "Test Account".to_string(),
            version: 1,
            events: vec![],
            ..Default::default()
        }
    }
}
DomainEvent — generates aggregate_id(), tenant_id(), event_type(), and event metadata accessors. Eliminates approximately 100 lines per entity. InMemoryRepository — generates a full CRUD implementation backed by a thread-safe Arc<RwLock<HashMap>>. Eliminates approximately 200 lines per entity. DynamoDbRepository — generates save, get, query, and delete methods with CapsuleClient integration, tenant isolation enforcement, and TransactWriteItems for atomic multi-item writes. Eliminates approximately 250 lines per entity. CachedRepository — generates a Moka-backed caching decorator with TTL expiration, LRU eviction, and configurable cache invalidation strategy. Eliminates approximately 150 lines per entity.

3.2 Defects Identified During Verification

A Verifier agent operating from a fresh context reviewed the initial macro implementation and identified three defects before any entity migration occurred:

Issue 1: Multi-Item Entity Pattern Not Supported

Builder implemented macros for single-item entities (one DynamoDB record per domain object). But some entities use multi-item pattern:
// Account has 2 DynamoDB items:
// 1. METADATA item (account details)
// 2. LIST_ITEM for queryable fields

// Macro didn't support generating queries for LIST_ITEM pattern
Why Builder missed this: The plan showed examples for single-item entities only. Multi-item pattern was mentioned in a footnote.Impact if shipped: Macros would only work for 4/7 entities. The other 3 (Account, Contact, Opportunity) would need manual repository implementations.Fix: Added #[multi_item] attribute support to DynamoDbRepository macro (3 hours)

Issue 2: GSI Query Methods Missing

DynamoDB entities have Global Secondary Indexes (GSI) for queries:
  • Query accounts by industry
  • Query contacts by company
  • Query opportunities by stage
Macro generated primary key queries but forgot GSI queries.Impact: Common query patterns still required manual implementation.Fix: Added GSI method generation with #[gsi_methods(gsi_name)] attribute (2 hours)

Issue 3: Cache Invalidation Strategy Unclear

CachedRepository macro generated write-through caching but didn’t specify invalidation strategy:
  • Should update invalidate cache immediately?
  • Should delete remove from cache?
  • What about bulk operations?
Impact: Inconsistent caching behavior across entities could lead to stale reads.Fix: Added explicit #[cache_strategy] attribute with options: write_through, write_around, write_back (1.5 hours)

3.3 Critical Security Defect in Generated Code

Manual inspection of the generated code using cargo expand identified a security defect not caught by the Verifier:
// Generated code (WRONG):
pub async fn save(&self, aggregate: Account) -> Result<()> {
    let entity = AccountEntity::from_domain(aggregate);
    let item = to_item(&entity)?;

    self.client.client()
        .put_item()
        .table_name(&self.table_name)
        .set_item(Some(item))
        .send()
        .await?;

    Ok(())  // ❌ Missing tenant isolation check
}
Without explicit validation that aggregate.tenant_id matches self.client.tenant_id(), the generated save() method would silently write an entity belonging to one tenant into the table partition of another tenant if the caller provided a mismatched aggregate. The corrected generated code:
// Generated code (CORRECT):
pub async fn save(&self, aggregate: Account) -> Result<()> {
    // NEW: Validate tenant isolation
    if aggregate.tenant_id() != self.client.tenant_id() {
        return Err(RepositoryError::TenantMismatch {
            expected: self.client.tenant_id(),
            actual: aggregate.tenant_id(),
        });
    }

    let entity = AccountEntity::from_domain(aggregate);
    // ... rest of save logic
}
Inspect generated code with cargo expand for every macro that touches security-critical paths. AI-generated macro logic does not inherit security constraints from the surrounding codebase—it produces code that satisfies the functional specification it was given. The specification must explicitly include security invariants, and the generated output must be manually verified to encode them correctly.

4. Migration

4.1 Methodology: Incremental Entity Migration

Migration proceeded one entity at a time, verifying completeness against the existing test suite before proceeding to the next entity. This approach was chosen explicitly over bulk migration, which would have made it impossible to attribute any test failures to a specific entity’s migration. The Account entity was migrated first as the template, producing the following result:
// BEFORE: 812 lines
// Account domain model (152 lines)
// InMemoryAccountRepository (215 lines)
// DynamoDbAccountRepository (295 lines)
// CachedAccountRepository (150 lines)

// AFTER: 43 lines
#[derive(DomainAggregate, Debug, Clone)]
#[id_type = "AccountId"]
#[event = "AccountEvent"]
pub struct Account {
    pub id: AccountId,
    pub name: String,
    pub industry: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(InMemoryRepository)]
#[aggregate = "Account"]
#[id_type = "AccountId"]
pub struct InMemoryAccountRepository;

#[derive(DynamoDbRepository)]
#[aggregate = "Account"]
#[entity = "AccountEntity"]
#[multi_item]
#[gsi_methods("by_industry")]
pub struct DynamoDbAccountRepository;

#[derive(CachedRepository)]
#[inner = "DynamoDbAccountRepository"]
#[cache_strategy = "write_through"]
pub struct CachedAccountRepository;
All 23 existing tests passed without modification.

4.2 Migration Results by Entity

EntityLines BeforeLines AfterReductionMigration Time
Account81243769 (95%)2h
Contact73438696 (95%)1.5h
Lead69841657 (94%)1.5h
Opportunity85647809 (94%)2h
Activity61235577 (94%)1h
Product72339684 (95%)1.5h
Address54232510 (94%)1h
Total4,9772754,702 (94%)11.5h
Migration time decreased for entities completed after Account, as the pattern was established and edge cases had been resolved in the macro.

4.3 Defect Surfaced During Migration

Attempting to migrate the final three entities (Product, Activity, Address) simultaneously—after four successful sequential migrations—resulted in 14 compilation errors. The Product entity introduced Decimal fields for pricing data; the DomainAggregate macro’s generated apply() method assumed all fields implemented Clone, which Decimal did not. AI Verifier analysis of the compilation errors identified the root cause:
“Product entity has Decimal fields for pricing. DomainAggregate macro generates apply() method that assumes all fields are Clone. Decimal type doesn’t implement Clone trait. Recommendation: Require where T: Clone bound on all fields.”
Adding the where T: Clone constraint to the generated method signature resolved all 14 errors. This defect would not have been identified during the incremental migration of Account through Opportunity, none of which used Decimal fields.

5. Comparative Analysis: Manual vs. Macro-Driven Repository Implementation

DimensionManual ImplementationMacro-Driven Implementation
Lines per entity600–85030–50
Time to add new entity4–6 hours1–1.5 hours
Bug fix propagationManual update to each entity (3.5 hours average)Single macro change, all entities updated (20 minutes)
Security invariant enforcementDepends on developerGenerated by macro, consistent across all entities
Discoverability of generated logicDirect (source is visible)Requires cargo expand
Compile-time error qualityDirect from Rust compilerDependent on macro error message quality
Abstraction learning curveNone (standard Rust)Requires understanding macro attribute semantics

6. Principles Established

Macros should encode invariants, not merely reduce typing. A macro that generates convenient boilerplate but permits incorrect patterns provides false confidence. The tenant isolation check in the generated save() method is the canonical example: the macro cannot be used to write a non-isolating save because the check is generated unconditionally. Explicit annotation outperforms inference in production macros. Inferential macros that detect field names or access patterns automatically are brittle under refactoring. Explicit attribute macros that require the developer to state intent directly produce clearer compiler errors and more predictable generated code. Generated code must be treated as code, not as a black box. The security defect identified through cargo expand would not have been caught by the functional test suite, because the tests operated on the generated methods—they could not see that the isolation check was absent. Security-critical invariants require direct inspection of generated code, not only behavioral testing. Migrate incrementally. Each entity migration is a test of the macro against the specific characteristics of that entity. Simultaneous bulk migration defers defect discovery and makes root cause attribution difficult. Incremental migration converts each entity into a macro integration test that improves the macro for subsequent entities. AI is more effective at implementing abstractions than designing them. The evaluator agent produced a high-quality pattern analysis and implementation plan; the implementation agent produced 2,917 lines of macro code that passed verification with three defects (all fixed before deployment). The abstraction boundary design—which patterns to expose as macros, which attributes to require, what invariants to encode—required human judgment. The implementation of those decisions was effectively delegated to AI.

7. Recommendations

  1. Do not write macros until five or more examples of the target pattern exist. Pattern extraction is the foundational step; AI pattern analysis requires sufficient examples to distinguish structural uniformity from coincidental similarity. Five examples provide the minimum corpus for reliable abstraction.
  2. Design the macro attribute API before implementation begins. The attribute names, required versus optional semantics, and error messages are the user interface for the macro. These decisions require human judgment about developer experience and architectural intent; they should not be delegated to implementation agents.
  3. Add compile-time validation for all required attributes and pattern constraints. Every required attribute should produce a diagnostic compile error with a corrective action and usage example if absent. Macros that fail silently or with generic errors impose debugging costs that negate the productivity benefit.
  4. Run cargo expand on the generated output for at least one entity before deploying any macro to production. This step is mandatory for macros that touch security-critical paths. The inspection should verify that security invariants (tenant isolation checks, encryption invocations, audit event emissions) are present in the generated code.
  5. Maintain macro API backward compatibility with versioned attribute semantics. Macro changes propagate to all consuming entities simultaneously. A breaking change in a macro attribute is a simultaneous breaking change across every entity in the codebase. Semantic versioning and documented deprecation cycles are required from the first external consumer.

8. Conclusion

Procedural macro code generation is a high-leverage technique for eliminating structurally uniform boilerplate in event-sourced domain architectures. The evidence from this analysis demonstrates a 94% line count reduction across seven entities, a 4× reduction in time-to-add-new-entity, and a maintenance model where bug fixes propagate from a single source to all consuming entities simultaneously. The technique carries specific risks: generated code can omit security invariants that are not part of the functional specification, error message quality requires explicit developer-experience investment, and breaking changes to macro APIs have amplified blast radius. Each of these risks has a known mitigation documented in the recommendations above. As AI-assisted development tooling continues to mature, the combination of AI-driven pattern extraction and human-designed abstraction boundaries will likely become the standard model for macro development. Future work should investigate automated detection of additional boilerplate patterns that have accumulated to macro-eligible scale, and systematic cargo expand integration into CI pipelines to enforce ongoing inspection of generated security-critical paths.
All content represents personal learning from personal projects. Code examples are sanitized and generalized. No proprietary information is shared. Opinions are my own and do not reflect my employer’s views.