NJ Municipality Lookup
CodebaseSrcDomain

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:

  1. Check cache for existing result
  2. If miss, call geocoding service
  3. Validate result (score threshold, NJ state check)
  4. Cache successful result
  5. Return geocoding result with municipality

Error Handling:

  • Throws AddressNotFoundError for 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:

  1. Validate query (min 3 characters, max 200)
  2. Call suggestions service
  3. 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 cache
  • getSuggestions - 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

  1. Define function signature with port dependencies
  2. Implement business logic using entities and ports
  3. Add error handling for failure cases
  4. Add logging for key events
  5. Write unit tests with fake services
  6. Document in this README

On this page