Skip to main content

0051: Anti-Corruption Layer for External Events

Date: 2025-11-22

Status: Accepted

Service: book-keeper

Context

Book-Keeper consumes events from numerous external microservices (Invoicing, Payroll, Orders, etc.). Each service has its own:

  • Data models and terminology
  • Event schemas
  • Versioning strategies
  • Domain concepts

If Book-Keeper's core domain directly depends on these external schemas:

  • Changes in external services break Book-Keeper
  • Domain logic becomes polluted with external concerns
  • Testing becomes difficult (need to mock external schemas)
  • Domain model loses clarity and cohesion

This is the classic "external system coupling" problem in microservices architectures.

Decision

Implement an Anti-Corruption Layer (ACL) as the sole entry point for all external events. The ACL protects the Book-Keeper domain from external influence.

Architecture

External Event Bus

Event Dispatcher (Router)

Translator (per event type)

Internal Command Bus (Book-Keeper domain)

Components

  1. Event Dispatcher: Subscribes to external event topics, routes to appropriate Translator

  2. Translators: One class per external event type

    • Input: External event schema (e.g., PayrollRunCompleted from payroll-service)
    • Validation: Schema validation, business rule checks
    • Translation: Maps external data model to Book-Keeper's RecordJournalEntryCommand
    • Output: Internal domain command
  3. Domain Purity: Core Book-Keeper domain (aggregates, command handlers) has ZERO knowledge of external events

Example

# External event from invoicing-service
class InvoicePaidEvent:
invoice_id: str
customer_id: str
amount: Decimal
payment_method: str

# Translator
class InvoicePaidTranslator(ITranslator):
def translate(self, event: InvoicePaidEvent) -> RecordJournalEntryCommand:
return RecordJournalEntryCommand(
entries=[
{"account": "1010", "debit": event.amount}, # Cash
{"account": "1200", "credit": event.amount}, # AR
],
reference=f"Invoice {event.invoice_id}",
...
)

Pluggable Translators

Note: While a plugin mechanism exists for adding Translators dynamically, this is not the preferred approach.Translators should be added directly to the codebase for better maintainability.

The plugin pattern (using Python entry points) was explored but adds unnecessary complexity.

Consequences

Positive

  • Domain Protection: Core domain remains pure, unaffected by external changes
  • Loose Coupling: External services can evolve independently without breaking Book-Keeper
  • Centralized Translation Logic: All external-to-internal mapping in one place
  • Testability: Can unit test Translators independently with mock external events
  • Explicit Boundaries: Clear separation of concerns between bounded contexts
  • Scalability: Can scale translator consumers independently using event bus consumer groups

Negative

  • Translation Overhead: Every external integration requires writing a Translator
  • Maintenance Burden: When external schemas change, Translators must be updated
  • Latency: Adds a translation step before domain command execution
  • Code Duplication: Similar translations may exist across multiple Translators

Neutral

  • Schema Versioning: Must handle external event version upgrades gracefully
  • Error Handling: Invalid external events must be sent to dead-letter queue, not crash service
  • Monitoring: Need observability into translation failures and conversion rates

Implementation Notes

Translator Interface

class ITranslator(ABC):
@abstractmethod
def can_handle(self, event_type: str) -> bool:
pass

@abstractmethod
def translate(self, event: dict) -> Command:
pass

Error Handling

If translation fails:

  1. Log error with full event context
  2. Send to dead-letter queue for manual review
  3. Do NOT crash service or retry indefinitely
  4. Emit metric for monitoring

Consumer Groups

Use NATS JetStream consumer groups (or Kafka consumer groups) to scale:

  • Multiple translator instances
  • Distribute load across instances
  • Automatic failover

References

  • Domain-Driven Design by Eric Evans (Chapter on Anti-Corruption Layer)
  • Implementing Domain-Driven Design by Vaughn Vernon (Context Mapping patterns)