Skip to content

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? ​

  1. Decoupled modules - Services don't import each other's internal types
  2. Future-proof - Same interface works for in-process and distributed
  3. Audit trail - Events provide natural audit log
  4. Async workflows - Notifications, emails can be async

Initial In-Memory Rationale ​

AspectIn-MemoryRabbitMQ
Operational overheadNoneHigh (deploy, monitor, backup)
LatencyNanosecondsMilliseconds
ScaleSingle processDistributed
ComplexityLowMedium

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 ​

go
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 ​

go
// 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:

CategoryEvents
Identityuser.registered, user.logged_in
DoctorFuture doctor-owned profile events; no publication from current stubs
Organizationorganization.created, member.invited, team.created
Subscriptionsubscription.created, billing.payment.succeeded
PatientFuture patient-owned profile events after authoritative commits; current stubs publish none
PrescriptionFuture creation/edit/issue/cancel events after authoritative commits; current stubs publish none
Admindoctor.verified, doctor.rejected
Notificationnotification.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

Documentation follows the source. Planned contracts are marked explicitly.