Service Layer Pattern

Routes are thin HTTP handlers; use-case logic lives in services. Since I#2013 the target topology is three layers — see Persistence Topology:

Architecture

Route (HTTP adapter) -> Service (actor, authz, validation, DTO) -> Domain module (operations + Drizzle)

Routes handle: request validation, HTTP status codes, response formatting. Services handle: ServiceActor, authorization, Zod validation, i18n error mapping, DTO shaping, use-case coordination. Domain modules (domain/<x>/, interim lib/<x>/) own all persistence. New service code must not import @/db — legacy violators are allowlisted and migrate on touch.

Service Directory

All services live in backend/src/services/ (148 files as of 2026-06). Each service has a matching interface in backend/src/interfaces/ (71 files including infrastructure interfaces).

ServiceResponsibility
buildings-service.tsBuilding CRUD, geometry, energy data
quote-service.tsQuote lifecycle, line items, pricing
hubspot/CRM sync — see HubSpot Integration
portal/External portal logic — see Portal
document-template/PDF template management — see PDF Templates
feedback/User feedback collection — see Feedback System
create-services.tsDI container assembly — see Dependency Injection

Route Service Access (MANDATORY)

Routes access services via Hono context. Always destructure directly:

const { documentRequests, portal } = c.var.services;
 
// NEVER alias:
// const requestService = c.var.services.documentRequests;

For infrastructure/cross-cutting concerns, use c.var.container:

const { infrastructure, crossCutting } = c.var.container;

Service Reuse

Services are injected via Dependency Injection, making them reusable across:

  • Routes — HTTP request handlers
  • Other services — cross-service orchestration

Not across job processors: since I#2013, processors are batch presentation and consume the domain layer directly as peer clients of services (ProcessorContainer has no services slice) — see Persistence Topology.

Interface Contract

Every service implements a corresponding interface from backend/src/interfaces/:

// backend/src/interfaces/buildings-service.ts
export interface IBuildingsService {
  getBuilding(id: string): Promise<Building>;
  createBuilding(data: CreateBuildingInput): Promise<Building>;
}

This enables testability and loose coupling.

See Also