Files
agentic-dev/docs/decisions/adr-012-lazar-conformance.md
Danijel Martinek bffc6a96b3 docs(plan-8): partial Lazar doc-update pass (CLAUDE.md, overview.md, ADR-012, Plan 7 annotations)
First slice of the Plan 8 deferred doc-update checklist:
- CLAUDE.md Key Conventions: factory-function use cases/controllers,
  entities/models/<x>.ts paths, .toDynamicValue DI bindings, direct
  injection in tests
- docs/architecture/overview.md data-flow box updated to factory style
  (controller resolved via container.get<IXController>; use case factory
  takes deps as args)
- docs/decisions/adr-012-lazar-conformance.md created — records the
  conformance decision and four intentional divergences
- docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md and the
  matching spec annotated with a "pre-Plan-8 layout" note pointing at
  the refactor log

Remaining Plan 8 doc-update items (root AGENTS.md, per-feature AGENTS.md,
adding-a-feature.md, tdd-workflow.md, testing-strategy.md,
vertical-feature-spec.md §6/§10, core-testing AGENTS.md) intentionally
paused — Plan 9 (input/output unification) will change overlapping
content, so resuming after Plan 9 lands avoids double-churn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:17:35 +02:00

171 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-012: Lazar Nikolov Pattern Conformance
**Status:** Accepted
**Date:** 2026-05-05
**Supersedes:** none — extends ADR-006 (vertical-feature-packages) and ADR-008 (per-feature DI containers)
**Spec:** `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md`
**Plan:** `docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md`
**Refactor log:** `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`
## Context
The vertical-feature monorepo refactor (ADRs 001-010) and the TDD
foundation (ADR-011) established Clean Architecture per feature, but
the per-layer code shape diverged from the canonical reference
implementation by Lazar Nikolov
([nikolovlazar/nextjs-clean-architecture](https://github.com/nikolovlazar/nextjs-clean-architecture)).
Specifically, before this ADR:
- Use cases called `<feature>Container.get()` inside their bodies
(locator pattern), making them hard to test in isolation.
- Controllers were multi-method classes (`articles.controller.ts` with
`getBySlug`, `create`, `list` methods on one symbol).
- Entities lived flat at `entities/<x>.ts`; errors at `entities/errors.ts`.
- Mock implementations used a `mock-` prefix (`mock-articles.repository.ts`),
separating them visually from the real impl.
- Real Payload-backed implementations used a `payload-` prefix
(`payload-articles.repository.ts`).
- Repository/service interface filenames used a `-` separator
(`articles-repository.interface.ts`) instead of the canonical dot
(`articles.repository.interface.ts`).
- Tests rebound the DI container in `beforeEach` rather than constructing
mocks and injecting directly.
## Decision
Bring every feature into structural conformance with Lazar's reference
pattern, with four intentional divergences (§Adaptations below).
### What we adopted
1. **Factory-function use cases and controllers** — every use case and
every controller is a factory: `(deps) => async (input) => result`.
Each file exports `export type I*UseCase = ReturnType<typeof xUseCase>`
(or the analogous `I*Controller`) so consumers can depend on the type
without depending on the impl.
2. **Entity layout**`entities/models/<x>.ts` (Zod schema + type)
and `entities/errors/<domain>.ts` (domain-grouped error classes) +
`entities/errors/common.ts` (`InputParseError`).
3. **Naming** — dot-separated qualifiers throughout:
- Real repo impl: `<noun>.repository.ts` (no `payload-` prefix)
- Mock repo impl: `<noun>.repository.mock.ts` (suffix, not prefix)
- Repo interface: `<noun>.repository.interface.ts`
- Service variants follow the same pattern.
4. **One controller per use case** — no multi-method controllers.
Each verb-noun pair has its own file: `sign-in.controller.ts`,
`get-articles.controller.ts`, `delete-media.controller.ts`.
5. **InversifyJS `.toDynamicValue()` for factory bindings:**
```typescript
bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
signInUseCase(
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
),
);
```
6. **Direct injection in tests** — construct mocks and pass them into the
factory; no container rebinding for unit/use-case/controller tests:
```typescript
const users = new MockUsersRepository();
const auth = new MockAuthenticationService(users);
const useCase = signInUseCase(users, auth);
```
7. **Real Payload-backed `UsersRepository` and `AuthenticationService`
for `auth`** — previously only mocks existed. Some methods on
`AuthenticationService` (session create/validate/invalidate) are
deferred behind `NotImplementedError` until the cookie-strategy
decision is finalized; see refactor log §7.
8. **`media` is now a complete Clean Architecture feature** — entities,
application, infrastructure, interface-adapters, DI, integrations/api,
factories, contract, feature test. Previously it was just a Payload
collection.
### Intentional divergences from the reference (kept from prior ADRs)
| Aspect | Reference | Ours | Reason |
|---|---|---|---|
| DI library | `@evyweb/ioctopus` | `inversify` | Already integrated; equivalent expressive power via `.toDynamicValue()`. |
| DI scope | One global `ApplicationContainer` | One per feature (`authContainer`, `blogContainer`, …) | Vertical-feature isolation (ADR-008). |
| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by ADR-011 / Plan 7; clearer per-file ownership. |
| Instrumentation | Sentry/observability service wrapping | Not adopted | Out of scope; revisit when observability becomes a requirement. |
`InputParseError` is also duplicated per feature (~6 lines × 5
features) instead of sharing a global class — feature independence
beats DRY for a class this small.
## Consequences
### Positive
- **Trivially testable use cases & controllers.** Factory functions
take deps as arguments — tests inject mocks directly, no container
involvement, no shared mutable state across tests.
- **One reason to change per controller file.** Single-responsibility
per file makes diffs and code review cleaner.
- **Type aliases (`I*UseCase`/`I*Controller`) decouple consumers.**
The tRPC router and any other caller depends on the type, not the
factory impl.
- **Naming consistency** with widely-shared community pattern, lowering
ramp-up cost for engineers familiar with the reference.
- **Real auth + media** complete the architectural symmetry — every
feature now demonstrates the full layer stack.
### Negative
- **More files.** Per-use-case controllers added ~6 controller files
across `blog` and `marketing-pages`; `media` added ~30 files from
scratch.
- **DI bindings are more verbose.** `.toDynamicValue()` blocks are
longer than `.to()` for class bindings. Acceptable in exchange for
factory-function purity.
- **Two AuthenticationService methods remain `NotImplementedError`**
pending session-cookie strategy. Documented in refactor log §7;
unblocks development that doesn't depend on session lifecycle.
- **Doc churn.** All references to old paths/patterns across guides,
per-feature AGENTS, and the spec required updating in this follow-up
pass.
## Alternatives considered
- **Adopt `@evyweb/ioctopus`** — rejected. Already on inversify; switching
DI libraries is high-risk for low gain.
- **Move tests to `tests/unit/` mirror layout** — rejected. Colocation
is established (ADR-011); `*.test.ts` next to source is unambiguous
per-file ownership.
- **Move to a single global container** — rejected. Per-feature
containers (ADR-008) are load-bearing for vertical-feature isolation.
- **Keep multi-method controllers** — rejected. The single-responsibility
controller-per-use-case pattern wins on readability and testability.
- **Skip real `AuthenticationService`** — rejected for `UsersRepository`
(keep mock only) — partial real impl with documented `NotImplementedError`
for session methods is the better trade because it unblocks the
`auth` integration without forcing a premature cookie-strategy choice.
## Acceptance verification (Task 10, 2026-05-05)
- All 325 tests passing (was 244 pre-Plan-8; +81 net, +33%).
- `pnpm typecheck`, `pnpm lint`, `pnpm turbo boundaries` clean.
- No `entities/<x>.ts` files at root level.
- No `mock-*.ts` files in feature packages.
- No `payload-*.ts` files anywhere.
- Every use case has `export type I*UseCase = ReturnType<typeof ...>`.
- Every controller has `export type I*Controller = ReturnType<typeof ...>`.
See refactor log for full file-by-file inventory.
## References
- Spec: `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md`
- Plan: `docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md`
- Refactor log: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`
- Reference repo: https://github.com/nikolovlazar/nextjs-clean-architecture
- Prior ADRs: ADR-006 (vertical-feature-packages), ADR-008 (per-feature DI containers), ADR-011 (TDD foundation)