Design Principles

The product is a satirical blog. The engineering is not casual. — Project Specification, Section 9

This document names the design principles that shape the codebase, maps each to concrete patterns and enforcement mechanisms, and explains when they apply. The principles were already embedded in ADRs, tooling, and agent charters before this document existed; this page collects them in one place so every contributor and agent starts from the same vocabulary.


Principles at a Glance

Principle One-liner Primary enforcement
Single Responsibility One job per unit — handler, agent, component, service Agent charters (ADR 0009), handler extraction pattern
Open/Closed Extend via configuration, not code changes Remote Config, @jjk/constants barrel
Dependency Inversion Depend on abstractions, not concrete libraries jjk-* wrappers (ADR 0007), Cloud Function DI
Interface Segregation Expose narrow interfaces; hide implementation Cloud Functions as write path (ADR 0003), Firestore rules
Don’t Repeat Yourself Single source of truth for every concept shared/ barrels (ADR 0006), copy.json (ADR 0010), boundary lint
Composition over Inheritance Compose behavior from small units; no class hierarchies DI in handlers, standalone Angular components

Liskov Substitution is intentionally absent. The codebase uses composition and dependency injection rather than inheritance hierarchies, so LSP has no surface area to apply.


Single Responsibility Principle

Rule: Each unit — handler, agent, component, service — does one thing.

Where it appears

  • Cloud Function handlers. Each exported handler (submitInboxMessageHandler, onRatingWrittenHandler, clockInHandler, etc.) owns exactly one write path. Handlers are extracted into dedicated files (handlers.ts, linkedin-handlers.ts, ernest-handlers.ts) away from wiring in index.ts.

  • Agent charters (ADR 0009). Every agent has an explicit scope boundary written as “Not X — that’s Y’s job.” Scout reviews the file in front of it; Cartographer maps structure; Inspector checks spec compliance. Overlap is a bug.

  • Angular components. One component per file, co-located template and styles, jjk- selector prefix. Components render; services fetch.

Enforcement

  • Agent descriptions include negative-scope statements.
  • /hygiene panel (scout, cartographer, inspector) flags handlers or components that accumulate unrelated responsibilities.

Open/Closed Principle

Rule: Extend behavior through configuration — not by modifying existing code.

Where it appears

  • Remote Config. Operator-tunable values (ernest_generation_model, admin_idle_timeout_ms) change runtime behavior without code changes or deploys.

  • @jjk/constants barrel. Shared constants are imported, not hardcoded. Changing a value in shared/constants/index.ts propagates everywhere without touching consumer code.

  • Content pipeline. Adding a blog post means adding a Markdown file. The build pipeline (tools/build/) processes it without code changes.

Enforcement

  • Inspector flags hardcoded values that belong in constants or Remote Config.
  • Build-time content pipeline means new content never requires code changes.

Dependency Inversion Principle

Rule: Depend on abstractions, not concrete implementations.

Where it appears

  • Cloud Function dependency injection. Every handler file exports a factory that accepts its dependencies as typed interfaces:

    // handlers.ts — real deps injected by index.ts; mocks injected by tests
    export function createHandlers(deps: HandlerDeps) { ... }

    Handler code never imports firebase-admin directly. It receives a Firestore type, a FieldValue shape, and an HttpsError constructor. Tests provide lightweight stubs matching those interfaces — no Firebase emulator required.

    This pattern is consistent across handlers.ts, linkedin-handlers.ts, and ernest-handlers.ts.

  • PrimeNG wrapper components (ADR 0007). Templates use <jjk-data-table>, never <p-table>. The wrapper makes PrimeNG “a swappable implementation detail” — replacing the library touches wrapper internals only.

Enforcement

  • /hygiene flags direct Firebase imports inside handler files.
  • ADR 0007 mandates jjk-* selectors; no PrimeNG selector appears in templates.
  • Boundary lint (tools/lint/check-shared-boundaries.ts) prevents shared/ from importing concrete consumer code.

Interface Segregation Principle

Rule: Expose narrow, purpose-specific interfaces. Clients should not depend on methods they don’t use.

Where it appears

  • Cloud Functions as canonical write path (ADR 0003). Firestore rules declare allow create, update, delete: if false on sensitive collections. Clients get a read-only interface to Firestore; all mutations go through typed callable functions with validation, rate limiting, and audit trails. The rules file is “a permissions manifest, not a business-logic document.”

  • Handler dependency interfaces. Each handler file defines the minimal shape it needs — FieldValueLike, TimestampLike, HttpsErrorCtor — not the full Firebase SDK surface. Tests only need to satisfy the narrow interface.

  • Path aliases. Consumers import @jjk/models, @jjk/constants, etc. — focused barrels, not a single @jjk/shared dump.

Enforcement

  • Firestore rules enforce the read-only client interface at the database layer.
  • Sentry agent audits frontend code for direct writes that bypass the function layer.

Don’t Repeat Yourself

Rule: Every concept has a single source of truth.

Where it appears

  • shared/ barrels (ADR 0006). Models, constants, utilities, and services each have one barrel (shared/*/index.ts) imported via path alias. “Adding shared code is moving a file and exporting it from a barrel. No generator, no build step, no version bump.”

  • copy.json (ADR 0010). All user-facing strings live in frontend/src/app/content/copy.json. Tone audits read one file. Typos in string keys fail the build via the derived AppCopy type.

  • CLAUDE.md guidance. “Grep before you build. If it belongs in shared/, put it there from the start.”

Enforcement

  • tools/lint/check-shared-boundaries.ts runs at build time and in CI. It verifies shared/ is leaf code — it may be imported by frontend/, functions/, and tools/, but must not import from any of them.
  • /hygiene panel flags duplicated logic and extraction candidates.
  • /pre-check verifies nothing already exists before new code is written.

Composition over Inheritance

Rule: Build behavior by composing small, replaceable units — not by extending base classes.

Where it appears

  • No class hierarchies. Angular components are standalone. Cloud Function handlers are plain functions accepting injected dependencies. There are no abstract base classes, no extends, no template method patterns.

  • Service composition. Frontend services are injected via Angular’s DI container. Backend handlers receive dependencies through factory functions. Behavior is assembled, not inherited.

Why

TypeScript and Angular’s modern patterns (standalone components, signals, functional guards) favor composition. Inheritance adds coupling and makes testing harder — the opposite of what the DI pattern in Cloud Functions achieves.


Judgment Calls

Principles are guidelines, not laws. These are the recognized exceptions:

Situation Guidance
Three similar lines vs. premature abstraction Three similar lines are fine. Extract only when a fourth instance appears or when the duplication crosses package boundaries.
Handler doing two related things If both operations must succeed or fail together (e.g., write + audit log), keeping them in one handler is correct SRP — the “one thing” is the transaction.
Hardcoded value used once A constant used in exactly one place does not need to move to @jjk/constants. Extract when a second consumer appears.
Direct Firestore write from client Allowed only when the write is high-frequency, low-value, and its validity is fully expressible in Firestore rules (see ADR 0003 ratings exception).
Skipping the wrapper layer If a third-party component is used in exactly one place and is unlikely to be swapped, a direct import is acceptable. Document the exception.

Cross-References

Topic Source
Architectural constraints Project Spec (Sections 3, 8, 9)
Cloud Functions write path ADR 0003
Shared folders & path aliases ADR 0006
PrimeNG isolation & wrappers ADR 0007
Multi-agent architecture ADR 0009
Centralized copy.json ADR 0010
Error visibility policy ADR 0019
Boundary lint tools/lint/check-shared-boundaries.ts
Frontend error surfacing beacon agent
Backend error observability canary agent
DI pattern (general handlers) functions/src/handlers.ts
DI pattern (LinkedIn) functions/src/linkedin-handlers.ts
DI pattern (Ernest AI) functions/src/ernest-handlers.ts