# 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`.
