Skip to content

Domain Event Patterns

Overview

Switchain Services uses an event-driven architecture for cross-boundary communication. When something significant happens in one bounded context, it publishes a Domain Event. Other contexts subscribe to these events via Event Subscribers.

Domain Events

Structure

Extend DomainEvent from @sws/common/domain. Include a static eventName using the format <context>.<subjectAction>:

typescript
// packages/cex-wallets/domain/events/CoinBlockchainCreatedDomainEvent.ts
import { DomainEvent } from '@sws/common/domain'

export class CoinBlockchainCreatedDomainEvent extends DomainEvent {
  static eventName = 'cex-wallets.coinBlockchainCreated'

  constructor(
    public readonly coinId: string,
    public readonly blockchain: string,
    occurredOn?: Date
  ) {
    super(CoinBlockchainCreatedDomainEvent.eventName, occurredOn)
  }
}

Location

packages/<context>/domain/events/[Subject][Action]DomainEvent.ts

Naming

  • Class: [Subject][Action]DomainEvent (e.g., CoinBlockchainCreatedDomainEvent)
  • eventName: '<context>.<subjectAction>' (e.g., 'cex-wallets.coinBlockchainCreated')

Publishing Events

Application services publish events after completing a use case:

typescript
import { CommonFactory } from '@sws/common'
import { CoinBlockchainCreatedDomainEvent } from '../../domain/events/CoinBlockchainCreatedDomainEvent'

export class CoinBlockchainCreator {
  constructor(
    private readonly repository: CoinBlockchainRepository,
    private readonly eventPublisher = CommonFactory.eventPublisher()
  ) {}

  async run({ coinId, blockchain }: { coinId: string; blockchain: string }): Promise<void> {
    const entity = CoinBlockchain.create({ coinId, blockchain })
    await this.repository.save(entity)

    await this.eventPublisher.publish(
      new CoinBlockchainCreatedDomainEvent(coinId, blockchain)
    )
  }
}

Event Subscribers

Structure

Implement DomainEventSubscriber<T> from @sws/common/domain:

typescript
// packages/cex-wallets/application/eventSubscribers/UpdateCexCoinsBlockchainsOnCoinBlockchainCreated.ts
import type { DomainEventSubscriber } from '@sws/common/domain'
import { LoggerFactory } from '@sws/common/logger'
import { CoinBlockchainCreatedDomainEvent } from '../../domain/events/CoinBlockchainCreatedDomainEvent'

export class UpdateCexCoinsBlockchainsOnCoinBlockchainCreated
  implements DomainEventSubscriber<CoinBlockchainCreatedDomainEvent>
{
  private logger = LoggerFactory.logger()

  subscribedTo() {
    return [CoinBlockchainCreatedDomainEvent]
  }

  async on(event: CoinBlockchainCreatedDomainEvent): Promise<void> {
    this.logger.info(
      `UpdateCexCoinsBlockchainsOnCoinBlockchainCreated coinId:${event.coinId}, blockchain:${event.blockchain}`
    )

    try {
      // React to the event — call application services or controllers
      await CexCoinBlockchainControllers.update.run('binance', event.coinId, event.blockchain)
    } catch (error) {
      this.logger.error(`Error handling CoinBlockchainCreatedDomainEvent`, {
        error: error instanceof Error ? error.message : String(error)
      })
    }
  }
}

Location

packages/<context>/application/eventSubscribers/[Action]On[Event].ts

Naming

  • Class: [Action]On[Event] (e.g., UpdateCexCoinsBlockchainsOnCoinBlockchainCreated)
  • Always wrap the handler body in try/catch to avoid crashing the subscriber queue

Registering Subscribers

Subscribers are registered in the consuming app's EventsFactory.ts:

typescript
// apps/common-worker/infrastructure/factories/EventsFactory.ts
import { UpdateCexCoinsBlockchainsOnCoinBlockchainCreated } from '@sws/cex-wallets'
import { DeactivatePairsOnCoinBlockchainDeleted } from '@sws/rates'
import { CommonFactory } from '@sws/common'
import type { DomainEvent, DomainEventSubscriber } from '@sws/common/domain'
import { LoggerFactory } from '@sws/common/logger'

export class EventsFactory {
  static async initialize(): Promise<DomainEventSubscriber<DomainEvent>[]> {
    const logger = LoggerFactory.logger()
    logger.info('EventsFactory.initialize starting...')

    const eventSubscriber = CommonFactory.eventSubscriber()
    const subscribers = EventsFactory.createSubscribers()

    await eventSubscriber.addSubscribers(subscribers)

    logger.info('Events initialized', {
      subscribers: subscribers.map((s) => s.constructor.name).join(', ')
    })

    return subscribers
  }

  private static createSubscribers(): DomainEventSubscriber<DomainEvent>[] {
    return [
      new UpdateCexCoinsBlockchainsOnCoinBlockchainCreated(),
      new DeactivatePairsOnCoinBlockchainDeleted()
      // Add new subscribers here
    ]
  }
}

Event Flow Example

CoinBlockchain created in admin UI
  └─> CoinBlockchainCreator.run()
        └─> eventPublisher.publish(CoinBlockchainCreatedDomainEvent)
              ├─> UpdateCexCoinsBlockchainsOnCoinBlockchainCreated (cex-wallets package)
              │     └─> Updates CEX coin blockchains
              └─> DeactivatePairsOnCoinBlockchainDeleted (rates package)
                    └─> Regenerates trading pairs

Events in This Codebase

EventPublished ByConsumed By
CoinBlockchainCreatedDomainEventcex-walletscex-wallets (UpdateCexCoinsBlockchains), rates
CoinBlockchainDeletedDomainEventcex-walletsrates (DeactivatePairs), cex-wallets
CexAccountBannedDomainEventcex-walletsalerts
ShiftTradesCompletedDomainEventshiftsalerts