Use Cases
Application business logic orchestrating entities, validation, and external services through ports. Use cases represent the "what" the application does.
Domain Use Cases (domain/use-cases/)
Application business logic orchestrating entities, validation, and external services through ports. Use cases represent the "what" the application does.
Use Cases
lookup-address.ts
Primary use case: Geocode an address to determine its municipality.
export async function lookupAddress(
address: AddressInput,
geocodingService: GeocodingServicePort,
cache: CacheServicePort,
): Promise<GeocodingResult>;
Flow:
- Check cache for existing result
- If miss, call geocoding service
- Validate result (score threshold, NJ state check)
- Cache successful result
- Return geocoding result with municipality
Error Handling:
- Throws
AddressNotFoundErrorfor no results or low confidence scores - Logs warnings for cache misses and low scores
- Propagates API errors (timeouts, invalid responses)
get-suggestions.ts
Autocomplete use case: Get address suggestions for partial input.
export async function getSuggestions(
query: string,
suggestionsService: SuggestionsServicePort,
): Promise<string[]>;
Flow:
- Validate query (min 3 characters, max 200)
- Call suggestions service
- Return array of suggestion strings
Features:
- Input validation before external call
- Empty array for invalid queries (no errors)
- Logging for API failures
Use Case Patterns
Dependency Injection
All dependencies injected as port interfaces:
// ❌ Don't import concrete implementations
import { njGeocodingClient } from "../../adapters/nj-api/geocoding-client";
// ✅ Do accept port interfaces
export async function lookupAddress(
address: AddressInput,
geocodingService: GeocodingServicePort, // Port interface
) {}
Single Responsibility
Each use case has one clear purpose:
lookupAddress- Geocode and cachegetSuggestions- Autocomplete
Don't mix concerns (e.g., don't add suggestion logic to lookup).
Error Handling
Use cases throw typed domain errors:
if (!result || result.score < 80) {
throw new AddressNotFoundError({
address: address.text,
score: result?.score,
});
}
Logging
Use cases log key events for observability:
logger.info("Successfully geocoded address", {
address: result.inputAddress,
municipality: result.municipality,
score: result.score,
});
Testing
Use cases are tested with fake implementations of ports:
import { createFakeServices } from "../__tests__/helpers/fake-services";
const { fakeGeocodingService, fakeCache } = createFakeServices();
fakeGeocodingService.mockResolvedAddress(mockResult);
const result = await lookupAddress(address, fakeGeocodingService, fakeCache);
expect(result.municipality).toBe("Newark");
Test Coverage:
- ✅ Happy path scenarios
- ✅ Cache hit vs miss paths
- ✅ Error conditions (not found, timeouts, invalid data)
- ✅ Edge cases (low scores, empty results)
- ✅ Logging verification
Adding New Use Cases
- Define function signature with port dependencies
- Implement business logic using entities and ports
- Add error handling for failure cases
- Add logging for key events
- Write unit tests with fake services
- Document in this README