Use Cases Overview
Use cases are application services that orchestrate workflows across multiple domain services. They represent the business processes and operations that users can perform in the system. All use cases have been migrated to use neverthrow Result<T, DomainError> patterns for explicit error handling.
What is a Use Case?
In Domain-Driven Design and Clean Architecture, a use case (also called an application service or interactor) is:
- A workflow that coordinates multiple domain services
- Contains NO business logic (delegates to domain services)
- Represents a single user goal or system operation
- Organized by subdomain for clear business context
- Returns
Result<T, DomainError>for explicit error handling
Use Case Structure
All use cases follow the neverthrow Result pattern:
import { Result, ok, err } from "neverthrow";
import type { DomainError } from "@shared/domain-errors";
import { validationError } from "@shared/domain-errors";
export class SomeActionUseCase {
constructor(
private domainService1: DomainService1,
private domainService2: DomainService2,
) {}
async execute(
request: SomeActionRequest,
): Promise<Result<ActionResult, DomainError>> {
// Validate input
if (!request.isValid()) {
return err(validationError("Invalid request data", "request"));
}
// Orchestrate domain services with Result handling
const step1 = await this.domainService1.operation(request.param1);
if (step1.isErr()) return step1;
const step2 = await this.domainService2.anotherOperation(step1.value);
if (step2.isErr()) return step2;
return ok(step2.value);
}
}
Key Characteristics
- Named as actions:
SetupFund,PurchaseUnits,RunEndOfDay - Have execute() method: Standard entry point for all use cases
- No business logic: Only orchestration and delegation
- Coordinate domains: Call multiple domain services
Organization by Subdomain
Use cases are organized in worker/use-cases/ by subdomain:
worker/use-cases/
├── fund/ # Fund management use cases
│ ├── setup-fund.use-case.ts
│ ├── migrate-fund.use-case.ts
│ └── csv-parsers.ts
├── investor/ # Investor transaction use cases
│ └── purchase-units.use-case.ts
├── portfolio/ # Portfolio transaction use cases
│ └── purchase-bond.use-case.ts
└── operations/ # Cross-cutting operational use cases
├── run-end-of-day.use-case.ts
└── end-of-day.repository.ts
Why Organize by Subdomain?
- Screaming Architecture: Folder structure reveals business purpose
- Clear Context: Related use cases grouped together
- Easy Navigation: Find use cases by business domain
- Scalability: Easy to add new use cases to appropriate subdomain
Current Use Cases
Fund Subdomain
SetupFund
- Purpose: Initialize a new fund with all required infrastructure
- Orchestrates: Fund, BankAccount, Investor, Accounting services
- Location:
worker/use-cases/fund/setup-fund.use-case.ts
Workflow:
- Create fund entity
- Set up bank accounts
- Create chart of accounts
- Configure default fees
- Process initial investors (optional)
MigrateFund
- Purpose: Transition an existing fund to the latest schema while preserving investor and bank account integrity
- Orchestrates: Fund, BankAccount, Investor, Accounting, Operations services
- Location:
worker/use-cases/fund/migrate-fund.use-case.ts - Error Type:
MigrationError- Custom error types for migration-specific failures
Workflow:
- Acquire coordinated locks for fund, investors, and bank accounts
- Validate source data against target schema versions
- Replay balances, holdings, and NAV snapshots into the new schema
- Reconcile ledger and investor totals post-migration
- Release locks and emit migration audit trail
Investor Subdomain
PurchaseUnits
- Purpose: Execute investor unit purchase transaction
- Orchestrates: Investor, Fund, Accounting, BankAccount services
- Location:
worker/use-cases/investor/purchase-units.use-case.ts - Error Type:
PurchaseUnitError- Custom error types for purchase failures
Workflow:
- Validate purchase request
- Verify fund and organization access
- Record unit purchase in holdings
- Create accounting journal entry
- Record bank account movement
Portfolio Subdomain
PurchaseBond
- Purpose: Execute bond purchase transaction for fund portfolio
- Orchestrates: BondPortfolio, Fund, Accounting, BankAccount services
- Location:
worker/use-cases/portfolio/purchase-bond.use-case.ts - Error Type:
PurchaseBondError- Custom error types for bond purchase failures
Workflow:
- Validate bond and fund details
- Check purchase amount limits and fund availability
- Record bond transaction in portfolio
- Calculate and record accrued interest
- Create accounting journal entry for bond purchase
- Record cash movement from bank account
Operations Subdomain
RunEndOfDay
- Purpose: Execute comprehensive end-of-day processing for a fund with 8 distinct phases
- Orchestrates: All financial services (Accounting, Fund, BankAccount, FDR, Portfolios, UnrealizedGainLoss)
- Location:
worker/use-cases/operations/run-end-of-day.use-case.ts - Error Type:
EODError- Comprehensive error types for EOD processing failures
8-Phase Workflow:
- Setup & Validation: Validate fund access, business date, and prevent duplicate processing
- Market Data Sync: Sync equity and bond securities data from DSE API
- Portfolio Valuation: Apply market prices to holdings and calculate current values
- Unrealized Gains/Losses: Calculate daily changes and create journal entries
- Interest Accruals: Process bank, FDR, and bond interest calculations
- Fee Processing: Calculate and accrue management, trustee, custodian, and regulatory fees
- Asset Management: Process depreciation and amortization for fixed assets
- Finalization: Calculate NAV and create fund snapshot
Error Handling:
- Custom error types for each phase
- Detailed error reporting with phase-specific context
- Graceful degradation when individual components fail
- Comprehensive audit logging for compliance
Adding New Use Cases
When adding a new use case:
-
Identify the subdomain: Where does this use case belong?
- Fund operations →
fund/ - Investor operations →
investor/ - Portfolio operations →
portfolio/ - Cross-cutting operations →
operations/
- Fund operations →
-
Name it as an action:
VerbNounformat (e.g.,RedeemUnits,UpdateFees) -
Create the file:
worker/use-cases/{subdomain}/{action}.use-case.ts -
Define custom error types: Create domain-specific error types for better error handling:
export type ActionError =
| ValidationError
| BusinessRuleError
| InsufficientResourcesError;
export interface ValidationError {
type: "ValidationError";
message: string;
field?: string;
}
export interface BusinessRuleError {
type: "BusinessRuleError";
rule: string;
message: string;
context?: Record<string, unknown>;
} -
Implement the Result pattern:
export interface ActionRequest {
// Input parameters
}
export interface ActionResult {
// Output data
}
export class ActionUseCase {
constructor() {} // Inject required domain services
async execute(
request: ActionRequest,
): Promise<Result<ActionResult, ActionError>> {
// Validate input
if (!request.isValid()) {
return err({
type: "ValidationError",
message: "Invalid request data",
field: "request",
});
}
// Orchestrate domain services with Result handling
const step1 = await this.domainService1.operation(request.param1);
if (step1.isErr()) return step1;
return ok(step1.value);
}
} -
Register in factory: Add to
ApplicationServicestype andcreateApplicationServices()function -
Write tests: Create test file in
test/use-cases/{subdomain}/with Result patterns
Use Cases vs Domain Services
| Aspect | Use Case | Domain Service |
|---|---|---|
| Purpose | Orchestrate workflows | Implement business logic |
| Logic | No business logic | Contains business rules |
| Error Handling | Result<T, DomainError> | Result<T, DomainError> or exceptions |
| Location | worker/use-cases/ | worker/services/ |
| Dependencies | Multiple domain services | Repositories + other services |
| Method | execute(): Result<T, E> | Various domain methods |
| Organization | By subdomain/feature | By domain |
| Custom Errors | Domain-specific errors | General DomainError types |
Best Practices
DO ✅
- Keep use cases thin: Delegate all logic to domain services
- Name clearly: Use verb-noun format (SetupFund, not FundSetup)
- One responsibility: Each use case should have one clear purpose
- Document workflow: Add JSDoc explaining the orchestration steps
- Group by subdomain: Keep related use cases together
DON'T ❌
- Don't add business logic: Use cases should only orchestrate
- Don't create god use cases: Break down complex workflows
- Don't bypass domain services: Always delegate to the domain layer
- Don't mix concerns: Keep use cases focused on one operation
- Don't organize by technical layer: Organize by business subdomain
Example: SetupFund Use Case
/**
* Setup Fund Use Case
*
* Orchestrates the complete fund setup workflow.
*/
export class SetupFundUseCase {
constructor(
private fundService: FundService,
private bankAccountService: BankAccountService,
private investorService: InvestorService,
private accountingService: AggregateAccountingService,
) {}
async execute(request: SetupFundRequest): Promise<SetupFundResult> {
// 1. Create fund (delegates to domain service)
const fund = await this.fundService.createFund({...});
// 2. Create bank accounts (delegates to domain service)
const bankAccounts = [];
for (const account of request.bankAccounts) {
const ba = await this.bankAccountService.createBankAccount({...});
bankAccounts.push(ba);
}
// 3. Set up accounting (delegates to domain service)
await this.ensureFundAccounts(fund.id, request.bankAccounts);
// 4. Configure fees (delegates to domain service)
const fees = this.fundService.createDefaultBangladeshFees();
await this.fundService.updateFundFees(fund.id, fees);
return { fund, bankAccounts };
}
}
See Also
- Coordinators Overview - Coordinator implementation details
- Service Factory - How use cases are created
- DDD Approach - Domain-driven design principles