Skip to main content

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

  1. Named as actions: SetupFund, PurchaseUnits, RunEndOfDay
  2. Have execute() method: Standard entry point for all use cases
  3. No business logic: Only orchestration and delegation
  4. 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?

  1. Screaming Architecture: Folder structure reveals business purpose
  2. Clear Context: Related use cases grouped together
  3. Easy Navigation: Find use cases by business domain
  4. 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:

  1. Create fund entity
  2. Set up bank accounts
  3. Create chart of accounts
  4. Configure default fees
  5. 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:

  1. Acquire coordinated locks for fund, investors, and bank accounts
  2. Validate source data against target schema versions
  3. Replay balances, holdings, and NAV snapshots into the new schema
  4. Reconcile ledger and investor totals post-migration
  5. 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:

  1. Validate purchase request
  2. Verify fund and organization access
  3. Record unit purchase in holdings
  4. Create accounting journal entry
  5. 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:

  1. Validate bond and fund details
  2. Check purchase amount limits and fund availability
  3. Record bond transaction in portfolio
  4. Calculate and record accrued interest
  5. Create accounting journal entry for bond purchase
  6. 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:

  1. Setup & Validation: Validate fund access, business date, and prevent duplicate processing
  2. Market Data Sync: Sync equity and bond securities data from DSE API
  3. Portfolio Valuation: Apply market prices to holdings and calculate current values
  4. Unrealized Gains/Losses: Calculate daily changes and create journal entries
  5. Interest Accruals: Process bank, FDR, and bond interest calculations
  6. Fee Processing: Calculate and accrue management, trustee, custodian, and regulatory fees
  7. Asset Management: Process depreciation and amortization for fixed assets
  8. 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:

  1. Identify the subdomain: Where does this use case belong?

    • Fund operations → fund/
    • Investor operations → investor/
    • Portfolio operations → portfolio/
    • Cross-cutting operations → operations/
  2. Name it as an action: VerbNoun format (e.g., RedeemUnits, UpdateFees)

  3. Create the file: worker/use-cases/{subdomain}/{action}.use-case.ts

  4. 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>;
    }
  5. 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);
    }
    }
  6. Register in factory: Add to ApplicationServices type and createApplicationServices() function

  7. Write tests: Create test file in test/use-cases/{subdomain}/ with Result patterns

Use Cases vs Domain Services

AspectUse CaseDomain Service
PurposeOrchestrate workflowsImplement business logic
LogicNo business logicContains business rules
Error HandlingResult<T, DomainError>Result<T, DomainError> or exceptions
Locationworker/use-cases/worker/services/
DependenciesMultiple domain servicesRepositories + other services
Methodexecute(): Result<T, E>Various domain methods
OrganizationBy subdomain/featureBy domain
Custom ErrorsDomain-specific errorsGeneral DomainError types

Best Practices

DO ✅

  1. Keep use cases thin: Delegate all logic to domain services
  2. Name clearly: Use verb-noun format (SetupFund, not FundSetup)
  3. One responsibility: Each use case should have one clear purpose
  4. Document workflow: Add JSDoc explaining the orchestration steps
  5. Group by subdomain: Keep related use cases together

DON'T ❌

  1. Don't add business logic: Use cases should only orchestrate
  2. Don't create god use cases: Break down complex workflows
  3. Don't bypass domain services: Always delegate to the domain layer
  4. Don't mix concerns: Keep use cases focused on one operation
  5. 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