# ADR-036 — Geschichte responses are views assembled in-transaction, never entities **Status:** Accepted **Date:** 2026-06-10 **Issue:** #753 (JourneyEditor frontend), PR #792 review ## Context The project convention (CLAUDE.md §DTOs) has been: *"Response types are the model entities themselves (no response DTOs)."* That convention assumed entities whose associations are either eager or initialized by the time Jackson serializes. The lazy-fetch migration (ADR-022, `open-in-view: false`) broke that assumption: Jackson serializes **after** the service transaction has closed, so any lazy collection on a returned entity is a dead proxy. `Geschichte.items` (added with the Lesereisen data model, #750) made this concrete: every `PATCH /api/geschichten/{id}` (save draft, publish) failed with HTTP 500 `LazyInitializationException: Geschichte.items … (no session)`. Per-endpoint force-initialization (`g.getItems().size()` inside the transaction) worked for `getById()` but is a footgun: every new write method must remember the trick, the entity carries a warning comment nobody reads, and the raw entity also leaks the `author` `AppUser` graph (email, password hash, groups). ## Decision In the **geschichte domain**, controllers never return entities. Every response is a purpose-built read model assembled **inside** the service transaction: - `GET /api/geschichten` → `GeschichteSummary` (projection; never carries items; author exposes names only — never email) - `GET /api/geschichten/{id}` → `GeschichteView` (with `AuthorView`, `PersonView`, `JourneyItemView` items) - `POST /api/geschichten`, `PATCH /api/geschichten/{id}` → `GeschichteView` - JourneyItem endpoints → `JourneyItemView` The invariant: **entities never cross the controller boundary in this domain.** A view is constructed while the Hibernate session is open, so serialization can never touch a lazy proxy, and the response shape is an explicit, security-reviewed contract. ## Alternatives rejected - **`@Transactional` on read/write methods + force-init (`getItems().size()`)** — fixes one endpoint at a time, silently regresses when the next write method is added, and still serializes the raw `AppUser` author graph. - **`open-in-view: true`** — re-opens the session during rendering; hides N+1 queries and couples the HTTP layer to Hibernate session lifetime. Rejected already by ADR-022. - **Jackson `@JsonIgnore` on lazy fields** — loses the data the client needs (items ARE the journey) instead of loading it deliberately. ## Consequences - CLAUDE.md §DTOs names the geschichte domain as the exception to the entities-as-responses convention. Other domains (document, person, tag) still return entities; they predate ADR-022's lazy collections on their hot paths and migrate opportunistically when they grow lazy collections of their own. - `npm run generate:api` must run after any view change — the generated `Geschichte` schema no longer exists; frontend consumers use `GeschichteView`/`GeschichteSummary`. - New geschichte endpoints must add a view (or extend an existing one), not return the entity. The regression guards are `GeschichteHttpTest` (`update_returns_200_and_serializes_items_open_in_view_false`) and `GeschichteListProjectionTest`.