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
- Interface Segregation: Each port has a single, focused responsibility
- Stable Abstractions: Ports change less frequently than implementations
- Dependency Inversion: High-level domain depends on abstractions, not details
- Contract Testing: Implementations are tested against port contracts
Errors
Type-safe error classes for domain-specific failure modes. All errors are created using the `error-factory` pattern for consistency and type safety.
Use Cases
Application business logic orchestrating entities, validation, and external services through ports. Use cases represent the "what" the application does.