08: Architecture placement¶
Physical placement depends on the target surface:
| Surface | Core placement | Ports | Adapters and wiring |
|---|---|---|---|
| Consumer frontend | business_logic/domain, business_logic/application | business_logic/inbound_ports, business_logic/outbound_ports | infrastructure/outbound_adapters, infrastructure/config; Svelte routes/components drive inbound ports |
| TypeScript services | src/domain, src/application/use-cases | driven ports in src/application/ports; driving contract beside its use case | src/adapters/{inbound,outbound}, composition-root.ts |
| Passport, Fridge, Shopping List | one 'internal/ |
driven ports in the core's ports.go; no driving interface | internal/adapters/{inbound,outbound}, wired by cmd/server |
| Measurement | internal/measurement | core-owned driven ports | directional adapters and cmd/server |
| API Gateway | internal/gateway; no domain/use-case layer | not applicable | gateway package plus configuration |
The governing questions are what the code knows and which direction the dependency points. Purity alone does not make a function domain logic: a pure function that shapes an HTTP body still belongs to the HTTP adapter.
8.1: Frontend domain rule¶
export function computeFreshnessStatus(
expiry: string,
now: Date,
): 'fresh' | 'expiring' | 'expired' {
const expiryTime = Date.parse(expiry);
if (Number.isNaN(expiryTime)) throw new Error('invalid expiry date');
const days = Math.ceil((expiryTime - now.getTime()) / 86_400_000);
if (days <= 0) return 'expired';
if (days <= 5) return 'expiring';
return 'fresh';
}
Placement
*client/web-app/src/lib/business_logic/domain*. It is a pure freshness policy over supplied values; the caller supplies the clock, so the result is deterministic.8.2: Frontend scan orchestration¶
export class ResolveScan implements ScanResolver {
constructor(private readonly gateway: ScanGateway) {}
async resolve(params: ScanParams): Promise<ScanResponse | null> {
const response = await this.gateway.resolve(params);
return response ? overlayScannedItem(response, params) : null;
}
}
Placement
*business_logic/application*. It coordinates an outbound *ScanGateway* port and a domain mapping. The port itself belongs in *business_logic/outbound_ports*; the HTTP implementation belongs in *infrastructure/outbound_adapters/http*.8.3: HTTP request mapping¶
export function scanRequestBody(params: ScanParams): ScanRequestBody {
return {
gtin: params.gtin,
...(params.lot ? { lot: params.lot } : {}),
...(params.serial ? { serial: params.serial } : {}),
...(params.expiry ? { expiry: params.expiry } : {}),
};
}
Placement
The HTTP outbound adapter. Although pure, the function knows the wire schema of *POST /api/v1/scans*; that is protocol knowledge, not a domain invariant.8.4: TypeScript service port¶
export interface HealthProfileRepository {
findByAccount(accountId: string): Promise<HealthProfile | null>;
}
Placement
*src/application/ports/health-profile-repository.ts* in the applicable TypeScript service. The application use case consumes the interface; a Kysely implementation belongs under *src/adapters/outbound*.8.5: Go fallback orchestration¶
func (uc ResolveScan) resolveEntry(ctx context.Context, gtin string) (CatalogEntry, error) {
entry, err := uc.Catalog.GetByGTIN(ctx, gtin)
if err == nil {
return entry, nil
}
if !errors.Is(err, ErrUnknownProduct) {
return CatalogEntry{}, err
}
if fresh, ok := uc.lookupExternal(ctx, gtin); ok {
return fresh, nil
}
return CatalogEntry{}, ErrUnknownProduct
}
Placement
*services/passport-service/internal/passport*, the service's core package. It is application-style orchestration, but that layout intentionally colocates entities and use cases rather than creating a physical *application* package. *CatalogRepository* and *ProductSource* remain driven interfaces in the same core package's *ports.go*.8.6: Placement checklist¶
Before adding a file, answer:
- Which ADR layout applies to this service or client?
- Is the code a domain rule, use-case orchestration, a dependency contract, protocol translation, or composition?
- Which package consumes the interface?
- Does the proposed import point inward and preserve adapter-direction isolation?
- Is the concept already present in the canonical domain and contracts, or would it require a maintainer decision and ADR?