NJ Municipality Lookup
CodebaseSrcDomain

Ports

Interface definitions for external dependencies following the Ports & Adapters (Hexagonal) architecture pattern. The domain defines contracts; adapters implemen

Domain Ports (domain/ports/)

Interface definitions for external dependencies following the Ports & Adapters (Hexagonal) architecture pattern. The domain defines contracts; adapters implement them.

Purpose

Ports allow the domain to depend on abstractions rather than concrete implementations, enabling:

  • Testability: Mock implementations for unit tests
  • Flexibility: Swap implementations without changing business logic
  • Independence: Domain doesn't know about infrastructure details

Port Interfaces

geocoding-service.ts

Contract for geocoding operations.

export interface GeocodingServicePort {
  geocodeAddress(address: AddressInput): Promise<GeocodingResult>;
  geocodeBatch(addresses: AddressInput[]): Promise<GeocodingResult[]>;
}

Implementations: adapters/nj-api/geocoding-client.ts

suggestions-service.ts

Contract for address autocomplete.

export interface SuggestionsServicePort {
  getSuggestions(query: string): Promise<string[]>;
}

Implementations: adapters/nj-api/suggestions-client.ts

cache-service.ts

Contract for caching operations with statistics.

export interface CacheServicePort {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, value: T, ttlDays?: number): Promise<CacheEntry<T>>;
  delete(key: string): Promise<boolean>;
  clear(): Promise<number>;
  getStats(): Promise<CacheStats>;
}

Implementations: adapters/cache/in-memory-cache.ts, adapters/cache/cache-factory.ts (no-op)

http-client.ts

Contract for HTTP operations with retry logic.

export interface HttpClientPort {
  request<T>(
    url: string,
    options: HttpRequestOptions,
  ): Promise<HttpResponse<T>>;
  get<T>(url: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
  post<T>(
    url: string,
    body: unknown,
    options?: HttpRequestOptions,
  ): Promise<HttpResponse<T>>;
}

Implementations: adapters/http/fetch-client.ts

logger.ts

Contract for structured logging.

export interface LoggerPort {
  debug(message: string, context?: LogContext): void;
  info(message: string, context?: LogContext): void;
  warn(message: string, context?: LogContext): void;
  error(message: string, context?: LogContext): void;
}

Implementations: lib/logger.ts (console-based, could be swapped for Winston, Pino, etc.)

Dependency Injection Pattern

Use cases receive port implementations as parameters:

// Use case signature
export async function lookupAddress(
  address: AddressInput,
  geocodingService: GeocodingServicePort, // ← Port interface
  cache: CacheServicePort, // ← Port interface
): Promise<GeocodingResult> {
  // Business logic uses interfaces, not concrete types
}

// Caller provides implementations
const result = await lookupAddress(
  address,
  njGeocodingClient, //Adapter implementing GeocodingServicePort
  inMemoryCache, //Adapter implementing CacheServicePort
);

Testing with Fakes

Ports enable isolated testing with fake implementations:

// Test helper creates fake services
const { fakeGeocodingService, fakeCache } = createFakeServices();

// Configure fake behavior
fakeGeocodingService.mockResolvedAddress(mockResult);

// Test use case in isolation
const result = await lookupAddress(address, fakeGeocodingService, fakeCache);
expect(result.municipality).toBe("Newark");

Design Principles

  1. Interface Segregation: Each port has a single, focused responsibility
  2. Stable Abstractions: Ports change less frequently than implementations
  3. Dependency Inversion: High-level domain depends on abstractions, not details
  4. Contract Testing: Implementations are tested against port contracts

On this page