Service Patterns
Overview
Services are the main unit of use-case logic. There are two types:
- Domain Services — Pure business logic in the domain layer
- 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
LoggerFactoryfrom@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 })
}
}