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/' package containing entities and use cases 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:

  1. Which ADR layout applies to this service or client?
  2. Is the code a domain rule, use-case orchestration, a dependency contract, protocol translation, or composition?
  3. Which package consumes the interface?
  4. Does the proposed import point inward and preserve adapter-direction isolation?
  5. Is the concept already present in the canonical domain and contracts, or would it require a maintainer decision and ADR?