ADR-003: Event-Driven Architecture with Swappable Bus
Status
Accepted
Context
Modules in our monolith need to communicate without tight coupling. We needed to decide:
- Direct function calls: Simple but creates tight coupling
- Event bus (in-process): Decoupled, swappable implementation
- Message queue (RabbitMQ/Kafka): Production-ready but operational overhead
Decision
We use an event bus interface with swappable adapters. Production uses RabbitMQ; tests and documentation construction may use the in-memory adapter.
Rationale
Why Event-Driven?
- Decoupled modules - Services don't import each other's internal types
- Future-proof - Same interface works for in-process and distributed
- Audit trail - Events provide natural audit log
- Async workflows - Notifications, emails can be async
Initial In-Memory Rationale
| Aspect | In-Memory | RabbitMQ |
|---|---|---|
| Operational overhead | None | High (deploy, monitor, backup) |
| Latency | Nanoseconds | Milliseconds |
| Scale | Single process | Distributed |
| Complexity | Low | Medium |
The initial implementation used in-memory delivery for a small deployment. The production composition root has since selected RabbitMQ without changing service ownership or the public command/query contracts.
Interface Design
type Event struct {
Type string // "user.registered"
Payload map[string]string // {user_id, email, role}
OccurredAt time.Time
}
type Bus interface {
Publish(ctx context.Context, event Event) error
Subscribe(eventType string, handler func(Event)) error
}Implementation Swap Path
// Now (monolith)
eventBus := events.NewInMemoryBus()
// Distributed deployment
eventBus, err := events.NewRabbitMQBus(amqpURL)
if err != nil {
return err
}
defer eventBus.Close()Zero module code changes required.
Event Types
Events are categorized by domain:
| Category | Events |
|---|---|
| Identity | user.registered, user.logged_in |
| Doctor | Future doctor-owned profile events; no publication from current stubs |
| Organization | organization.created, member.invited, team.created |
| Subscription | subscription.created, billing.payment.succeeded |
| Patient | Future patient-owned profile events after authoritative commits; current stubs publish none |
| Prescription | Future creation/edit/issue/cancel events after authoritative commits; current stubs publish none |
| Admin | doctor.verified, doctor.rejected |
| Notification | notification.sent, notification.failed |
Billing event names above describe future after-commit notifications, not synchronous command transport or payment gateways. Admin billing commands use subscription/public capabilities (ADR-012/014); manually recording a payment does not activate or extend access. Current 501 billing, doctor, patient and prescription stubs publish no events. The production composition root already uses RabbitMQ; in-memory remains available for tests and documentation construction.
Consequences
Positive
- Modules are decoupled
- Easy to swap implementation
- Natural audit trail
- Ready for microservices extraction
Negative
- Eventual consistency (not immediate)
- Need to handle event failures
Related Decisions
ADR-001: Modular Monolith Architecture