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 inindex.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.
/hygienepanel (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/constantsbarrel. Shared constants are imported, not hardcoded. Changing a value inshared/constants/index.tspropagates 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-admindirectly. It receives aFirestoretype, aFieldValueshape, and anHttpsErrorconstructor. Tests provide lightweight stubs matching those interfaces — no Firebase emulator required.This pattern is consistent across
handlers.ts,linkedin-handlers.ts, andernest-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
/hygieneflags 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) preventsshared/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 falseon 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/shareddump.
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 infrontend/src/app/content/copy.json. Tone audits read one file. Typos in string keys fail the build via the derivedAppCopytype.CLAUDE.md guidance. “Grep before you build. If it belongs in shared/, put it there from the start.”
Enforcement
tools/lint/check-shared-boundaries.tsruns at build time and in CI. It verifiesshared/is leaf code — it may be imported byfrontend/,functions/, andtools/, but must not import from any of them./hygienepanel flags duplicated logic and extraction candidates./pre-checkverifies 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 |