Skip to content

Service Patterns

Overview

Services are the main unit of use-case logic. There are two types:

  1. Domain Services — Pure business logic in the domain layer
  2. Application Services — Orchestrate domain objects to fulfill use cases

Both ALWAYS have a public run() method as the main entry point.

Critical Rules

  • Entry Point: run() is the only public method that executes the use case
  • Single Responsibility: One service, one purpose
  • Constructor Injection: All dependencies via constructor
  • Logging: Use LoggerFactory from @sws/common/logger
  • Object args: When run() takes >2 arguments, use an object parameter

Domain Services

Located in packages/<context>/domain/services/. Pure business logic, no external dependencies:

typescript
// packages/rates/domain/services/PairsBuilder.ts
export class PairsBuilder {
  run({
    coins,
    blockchains,
    settings
  }: {
    coins: Coin[]
    blockchains: CoinBlockchain[]
    settings: PairSettings[]
  }): Pair[] {
    // Pure business logic — builds trading pairs from coins and blockchains
    return coins.flatMap((coin) =>
      blockchains
        .filter((bc) => bc.coinId.equals(coin.id))
        .map((bc) => Pair.create({ coin, blockchain: bc }))
    )
  }
}

Application Services

Located in packages/<context>/application/services/. Orchestrate domain objects:

typescript
// packages/cex-wallets/application/services/CoinBlockchainCreator.ts
import { LoggerFactory } from '@sws/common/logger'
import type { EventPublisher } from '@sws/common/domain'
import { CoinBlockchain } from '../../domain/entities/CoinBlockchain'
import { CoinBlockchainCreatedDomainEvent } from '../../domain/events/CoinBlockchainCreatedDomainEvent'
import type { CoinBlockchainRepository } from '../../domain/repositories/CoinBlockchainRepository'

export class CoinBlockchainCreator {
  private logger = LoggerFactory.logger()

  constructor(
    private readonly repository: CoinBlockchainRepository,
    private readonly eventPublisher: EventPublisher
  ) {}

  async run({ coinId, blockchain }: { coinId: string; blockchain: string }): Promise<CoinBlockchain> {
    this.logger.info(`CoinBlockchainCreator.run coinId:${coinId}, blockchain:${blockchain}`)

    const entity = CoinBlockchain.create({ coinId, blockchain })
    await this.repository.save(entity)

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

    return entity
  }
}

Logging Convention

Log at the start of run() with key identifiers. Use the format ClassName.run key:value:

typescript
this.logger.info(`CexCoinBlockchainStatusUpdater.run exchange:${exchange}, coinId:${coinId}`)

For errors, include the error message:

typescript
this.logger.error(`Error in CoinBlockchainCreator.run coinId:${coinId}`, {
  error: error instanceof Error ? error.message : String(error)
})

Factory Integration

Services should be accessible via their bounded context factory with optional TestDependencyOverrides:

typescript
// packages/cex-wallets/infrastructure/factories/CexCoinBlockchainFactory.ts
import type { TestDependencyOverrides } from '@sws/common/test'
import { CommonFactory } from '@sws/common'
import { CoinBlockchainCreator } from '../../application/services/CoinBlockchainCreator'
import { CexCoinBlockchainPrismaRepository } from '../repositories/CexCoinBlockchainPrismaRepository'

export class CexCoinBlockchainFactory {
  private static repositoryInstance: CexCoinBlockchainPrismaRepository

  static coinBlockchainCreator(overrides?: TestDependencyOverrides) {
    return new CoinBlockchainCreator(
      CexCoinBlockchainFactory.coinBlockchainRepository(overrides),
      CommonFactory.eventPublisher()
    )
  }

  static coinBlockchainRepository(overrides?: TestDependencyOverrides) {
    if (overrides?.coinBlockchainRepository) return overrides.coinBlockchainRepository
    if (!CexCoinBlockchainFactory.repositoryInstance) {
      CexCoinBlockchainFactory.repositoryInstance = new CexCoinBlockchainPrismaRepository(prisma)
    }
    return CexCoinBlockchainFactory.repositoryInstance
  }
}

Controllers (Thin Layer)

Controllers are a thin delegation layer. They call service run() and return the result:

typescript
// packages/cex-wallets/infrastructure/controllers/CoinBlockchainCreatorController.ts
import type { CoinBlockchainCreator } from '../../application/services/CoinBlockchainCreator'

export class CoinBlockchainCreatorController {
  constructor(private readonly service: CoinBlockchainCreator) {}

  async run(coinId: string, blockchain: string): Promise<void> {
    return this.service.run({ coinId, blockchain })
  }
}