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
-
Event Dispatcher: Subscribes to external event topics, routes to appropriate Translator
-
Translators: One class per external event type
- Input: External event schema (e.g.,
PayrollRunCompletedfrom payroll-service) - Validation: Schema validation, business rule checks
- Translation: Maps external data model to Book-Keeper's
RecordJournalEntryCommand - Output: Internal domain command
- Input: External event schema (e.g.,
-
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:
- Log error with full event context
- Send to dead-letter queue for manual review
- Do NOT crash service or retry indefinitely
- 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
Related ADRs
- ADR-0049: Event-Driven Integration Pattern - Why we use events
- ADR-0002: Swappable Infrastructure via Adapters - General adapter pattern
References
- Domain-Driven Design by Eric Evans (Chapter on Anti-Corruption Layer)
- Implementing Domain-Driven Design by Vaughn Vernon (Context Mapping patterns)