English + German
English stays on the page. Click the button to show the German text under each question.
REST and MVC
This is the request path: HTTP → filters → DispatcherServlet → controller → advice → JSON. Interviews reward people who can walk that path without skipping Security or validation.
Deep chapters: Week 4.
Deutsch
REST und MVC
Das ist der Request-Pfad: HTTP → Filter → DispatcherServlet → Controller → Advice → JSON. Im Interview punkten Leute, die diesen Pfad gehen können, ohne Security oder Validierung zu überspringen.
Zum Vertiefen: Woche 4.
1. Walk through a request in Spring MVC.
Deutsch
Lauf einen Request durch Spring MVC.
Level: Mid · Listen for: DispatcherServlet, HandlerMapping, Adapter, return value handlers
Niveau: Mid · Darauf hören sie: DispatcherServlet, HandlerMapping, Adapter, Return-Value-Handler
Model answer
The servlet container (Tomcat) receives the request. After servlet filters (including Spring Security), it hits the DispatcherServlet.
The servlet asks a HandlerMapping which controller method matches, then a HandlerAdapter invokes it. Arguments are resolved (@PathVariable, @RequestBody, …). The return value is written by a HttpMessageConverter (JSON) or a view resolver.
Exceptions go to @ExceptionHandler / @ControllerAdvice. After that, the filter chain still runs on the way out.
Follow-ups
- Front controller pattern?
- What is a
HandlerInterceptorvs a servletFilter?
Trap: starting the story at the controller method.
Memory sentence: Filters first, then DispatcherServlet maps and adapts, then message conversion.
Study: MVC mental model
Musterantwort
Der Servlet-Container (Tomcat) nimmt den Request entgegen. Nach den Servlet-Filtern (inkl. Spring Security) landet er beim DispatcherServlet.
Das Servlet fragt ein HandlerMapping, welche Controller-Methode passt, dann ruft ein HandlerAdapter sie auf. Argumente werden aufgelöst (@PathVariable, @RequestBody, …). Den Rückgabewert schreibt ein HttpMessageConverter (JSON) oder ein View Resolver.
Exceptions gehen an @ExceptionHandler / @ControllerAdvice. Danach läuft die Filterkette auf dem Rückweg weiter.
Nachfragen
- Front-Controller-Pattern?
- Was ist ein
HandlerInterceptorvs. ein Servlet-Filter?
Falle: die Geschichte bei der Controller-Methode beginnen.
Merksatz: Zuerst Filter, dann mappt und adaptiert der DispatcherServlet, dann Message Conversion.
Lesen: MVC-Mental-Model
2. @Controller vs @RestController?
Deutsch
@Controller vs. @RestController?
Level: Junior · Listen for: @ResponseBody on the type
Niveau: Junior · Darauf hören sie: @ResponseBody auf dem Typ
Model answer
@Controller is a component whose methods typically return a view name. @RestController is @Controller + @ResponseBody on the class: the return value is the body, usually JSON.
For an API I use @RestController. For server-rendered HTML I use @Controller + templates. Mixing both in one class is possible with per-method @ResponseBody, but it is noisy.
Follow-ups
- Can a
@Controllerstill return JSON? (yes, method-level@ResponseBody) ResponseEntityvs a raw DTO?
Trap: “@RestController is for REST and @Controller cannot do HTTP.”
Memory sentence: @RestController writes the return value as the body.
Study: MVC mental model
Musterantwort
@Controller ist eine Component, deren Methoden meist einen View-Namen zurückgeben. @RestController ist @Controller + @ResponseBody auf der Klasse: der Rückgabewert ist der Body, meist JSON.
Für eine API nehme ich @RestController. Für serverseitig gerendertes HTML @Controller + Templates. Beides in einer Klasse mischen geht mit @ResponseBody pro Methode, wird aber unübersichtlich.
Nachfragen
- Kann ein
@Controllertrotzdem JSON liefern? (ja,@ResponseBodyauf der Methode) ResponseEntityvs. ein nacktes DTO?
Falle: „@RestController ist für REST, und @Controller kann kein HTTP.“
Merksatz: @RestController schreibt den Rückgabewert als Body.
Lesen: MVC-Mental-Model
3. @RequestBody vs @RequestParam vs @PathVariable vs @ModelAttribute?
Deutsch
@RequestBody vs. @RequestParam vs. @PathVariable vs. @ModelAttribute?
Level: Mid · Listen for: where the data lives
Niveau: Mid · Darauf hören sie: wo die Daten liegen
Model answer
| Annotation | Source |
|---|---|
@PathVariable | URI template /orders/{id} |
@RequestParam | query string or form field |
@RequestHeader / @CookieValue | headers / cookies |
@RequestBody | decoded body (JSON via Jackson) |
@ModelAttribute | form binding onto an object, also used in MVC models |
JSON APIs: path + query + @RequestBody. Do not @RequestBody a GET. Do not put sensitive data in query strings (logs, proxies).
Follow-ups
required = falsevsOptional<T>parameters?consumes/produces?
Trap: binding a JSON body with @ModelAttribute.
Memory sentence: Path, query, headers, then body — pick the annotation that matches the HTTP place.
Study: Request mapping
Musterantwort
| Annotation | Quelle |
|---|---|
@PathVariable | URI-Template /orders/{id} |
@RequestParam | Query-String oder Form-Feld |
@RequestHeader / @CookieValue | Header / Cookies |
@RequestBody | dekodierter Body (JSON über Jackson) |
@ModelAttribute | Form Binding auf ein Objekt, auch in MVC-Models |
JSON-APIs: Path + Query + @RequestBody. Kein @RequestBody bei GET. Keine sensiblen Daten in Query-Strings (Logs, Proxies).
Nachfragen
required = falsevs.Optional<T>-Parameter?consumes/produces?
Falle: einen JSON-Body mit @ModelAttribute binden.
Merksatz: Path, Query, Header, dann Body — nimm die Annotation, die zur Stelle im HTTP-Request passt.
Lesen: Request Mapping
4. How does Jackson become the JSON converter?
Deutsch
Wie wird Jackson zum JSON-Converter?
Level: Mid · Listen for: HttpMessageConverter, consumes/produces
Niveau: Mid · Darauf hören sie: HttpMessageConverter, consumes/produces
Model answer
Boot auto-configures MappingJackson2HttpMessageConverter when Jackson is on the classpath. MVC picks a converter from Content-Type (input) and Accept / produces (output).
If Accept is application/xml and you have no XML converter, you get 406. If Content-Type is JSON but the body is invalid, you get 400 before your method runs.
I keep DTOs Jackson-friendly: records or explicit properties, no lazy Hibernate entities (that is how you get ByteBuddyInterceptor JSON and accidental lazy loads).
Follow-ups
- Custom
ObjectMapperbean — does Boot back off? @JsonIgnorevs dedicated response DTOs?
Trap: returning entities and “fixing” lazy fields with @JsonIgnoreProperties.
Memory sentence: Message converters are selected by media type; never serialize JPA entities.
Study: Request and response bodies
Musterantwort
Boot auto-konfiguriert MappingJackson2HttpMessageConverter, wenn Jackson auf dem Classpath liegt. MVC wählt den Converter anhand von Content-Type (Request) und Accept / produces (Response).
Ist Accept application/xml und du hast keinen XML-Converter, kommt 406. Ist Content-Type JSON, der Body aber ungültig, kommt 400, bevor deine Methode läuft.
DTOs halte ich Jackson-tauglich: Records oder explizite Properties, keine lazy Hibernate-Entities (sonst landet ByteBuddyInterceptor im JSON und du lädst ungewollt lazy Assoziationen).
Nachfragen
- Eigener
ObjectMapper-Bean — macht Boot Back-off? @JsonIgnorevs. eigene Response-DTOs?
Falle: Entities zurückgeben und lazy Felder mit @JsonIgnoreProperties „reparieren“.
Merksatz: Message Converter werden nach Media Type gewählt; JPA-Entities nie serialisieren.
Lesen: Request- und Response-Bodies
5. How do you handle exceptions in a REST API?
Deutsch
Wie behandelst du Exceptions in einer REST-API?
Level: Mid · Listen for: @ControllerAdvice, status codes, not a generic 500
Niveau: Mid · Darauf hören sie: @ControllerAdvice, Statuscodes, kein pauschales 500
Model answer
Service code throws domain exceptions. A @RestControllerAdvice maps them:
- not found → 404
- conflict / duplicate → 409
- validation → 400
- authz → 403
- unexpected → 500, logged with a correlation id, no stack trace in the body
ResponseEntityExceptionHandler already covers many MVC/Boot errors. I extend it instead of fighting it.
I do not catch exceptions in every controller method. That duplicates policy.
Follow-ups
@ExceptionHandleron a controller vs global advice?- RFC 7807
ProblemDetail(Boot 3)?
Trap: catch (Exception e) { return 200; } or leaking SQL messages to clients.
Memory sentence: Throw domain exceptions; translate them once at the HTTP boundary.
Study: Exception handling
Musterantwort
Service-Code wirft Domain Exceptions. Ein @RestControllerAdvice mappt sie:
- nicht gefunden → 404
- Konflikt / Duplikat → 409
- Validierung → 400
- authz → 403
- unerwartet → 500, mit Correlation-Id geloggt, kein Stacktrace im Body
ResponseEntityExceptionHandler deckt schon viele MVC-/Boot-Fehler ab. Ich erweitere ihn, statt dagegen zu kämpfen.
Ich fange Exceptions nicht in jeder Controller-Methode. Dann ist die Fehlerbehandlung doppelt.
Nachfragen
@ExceptionHandlerauf einem Controller vs. globales Advice?- RFC 7807
ProblemDetail(Boot 3)?
Falle: catch (Exception e) { return 200; } oder SQL-Meldungen an Clients durchlassen.
Merksatz: Domain Exceptions werfen; einmal an der HTTP-Grenze übersetzen.
Lesen: Exception Handling
6. @Valid vs @Validated? Where does validation run?
Deutsch
@Valid vs. @Validated? Wo läuft Validierung?
Level: Mid · Listen for: method argument resolver, groups, 400
Niveau: Mid · Darauf hören sie: Method Argument Resolver, Groups, 400
Model answer
@Valid (Jakarta) on a @RequestBody DTO triggers Bean Validation before the controller body. Failures become MethodArgumentNotValidException → 400.
@Validated is Spring’s variant: it enables validation groups and can validate on @Service methods (AOP). I use @Valid on controllers and @Validated on the class when groups are needed.
@NotNull on a @RequestParam is not the same as a DTO field — you still need @Validated on the controller for constraint annotations on parameters.
Never skip validation because “the frontend checks it”.
Follow-ups
- Why is
@Validon a nested object not enough without@Validon the nested field? @Validatedon a service vs controller — proxy required?
Trap: putting @Valid on a GET DTO that is actually @RequestParam fields without @Validated.
Memory sentence: Validate at the boundary with @Valid; groups need @Validated.
Study: Validation
Musterantwort
@Valid (Jakarta) auf einem @RequestBody-DTO löst Bean Validation vor dem Controller-Body aus. Fehler werden zu MethodArgumentNotValidException → 400.
@Validated ist Springs Variante: es aktiviert Validation Groups und kann auf @Service-Methoden validieren (AOP). Ich nutze @Valid auf Controllern und @Validated auf der Klasse, wenn Groups nötig sind.
@NotNull auf einem @RequestParam ist nicht dasselbe wie auf einem DTO-Feld — für Constraint-Annotations auf Parametern brauchst du trotzdem @Validated auf dem Controller.
Validierung nie weglassen, weil „das Frontend prüft das schon“.
Nachfragen
- Warum reicht
@Validauf dem äußeren Objekt nicht ohne@Validam verschachtelten Feld? @Validatedauf einem Service vs. Controller — Proxy nötig?
Falle: @Valid auf ein GET-DTO setzen, das in Wahrheit @RequestParam-Felder sind, ohne @Validated.
Merksatz: An der Grenze mit @Valid validieren; Groups brauchen @Validated.
Lesen: Validation
7. Idempotency: PUT vs POST vs PATCH vs DELETE?
Deutsch
Idempotenz: PUT vs. POST vs. PATCH vs. DELETE?
Level: Mid · Listen for: HTTP semantics, not “we always POST”
Niveau: Mid · Darauf hören sie: HTTP-Semantik, nicht „wir machen immer POST“
Model answer
GET is safe and idempotent: no state change.
PUT replaces a resource at a known URI; repeating it yields the same state.
DELETE is idempotent: deleting twice is still “gone” (I return 204 both times, or 404 the second — pick a policy and keep it).
POST creates or triggers a process; repeating it may create two orders. For payments I send an Idempotency-Key.
PATCH is a partial update; it is not automatically idempotent unless you design it that way.
I do not use POST for everything because “it is easier”.
Follow-ups
- POST-Redirect-GET?
- How do you store idempotency keys?
Trap: “REST means JSON over POST.”
Memory sentence: Methods have semantics; POST is the one that needs extra idempotency for money.
Musterantwort
GET ist safe und idempotent: ändert keinen State.
PUT ersetzt eine Ressource an einer bekannten URI; Wiederholen liefert denselben State.
DELETE ist idempotent: zweimal löschen ist immer noch „weg“ (ich gebe beide Male 204, oder beim zweiten Mal 404 — eine Regel wählen und dabeibleiben).
POST erzeugt oder startet einen Prozess; Wiederholen kann zwei Orders anlegen. Bei Zahlungen schicke ich einen Idempotency-Key.
PATCH ist ein partielles Update; automatisch idempotent ist es nicht, außer du designst es so.
Ich mache nicht alles per POST, weil „es einfacher ist“.
Nachfragen
- POST-Redirect-GET?
- Wie speicherst du Idempotency Keys?
Falle: „REST heißt JSON über POST.“
Merksatz: Methoden haben Semantik; POST braucht bei Geld extra Idempotenz.
8. What status codes do you actually use?
Deutsch
Welche Statuscodes nutzt du wirklich?
Level: Mid · Listen for: 201 + Location, 204, 400 vs 422, 401 vs 403, 409
Niveau: Mid · Darauf hören sie: 201 + Location, 204, 400 vs. 422, 401 vs. 403, 409
Model answer
- 200 with a body
- 201 created,
Locationheader - 204 no body (delete / empty update)
- 400 malformed or validation
- 401 not authenticated
- 403 authenticated but not allowed
- 404 unknown resource (sometimes also for hidden resources)
- 409 conflict (version, duplicate)
- 429 rate limit
- 500 only for unexpected bugs
I do not invent 200 + { success: false }. That breaks HTTP clients and caches.
Follow-ups
- 422 vs 400?
- 404 vs 403 to hide existence?
Trap: 200 for every outcome.
Memory sentence: The status is part of the API; do not bury it in a JSON flag.
Musterantwort
- 200 mit Body
- 201 Created,
Location-Header - 204 kein Body (Delete / leeres Update)
- 400 ungültiger Request oder Validierung
- 401 nicht authentifiziert
- 403 authentifiziert, aber nicht erlaubt
- 404 unbekannte Ressource (manchmal auch für versteckte Ressourcen)
- 409 Konflikt (Version, Duplikat)
- 429 Rate Limit
- 500 nur für unerwartete Bugs
Ich erfinde nicht 200 + { success: false }. Das bricht HTTP-Clients und Caches.
Nachfragen
- 422 vs. 400?
- 404 vs. 403, um Existenz zu verstecken?
Falle: 200 für jedes Ergebnis.
Merksatz: Der Status gehört zur API; nicht in einem JSON-Flag vergraben.
9. Filter vs interceptor vs controller advice vs AOP?
Deutsch
Filter vs. Interceptor vs. Controller Advice vs. AOP?
Level: Senior · Listen for: where in the chain; what you can see
Niveau: Senior · Darauf hören sie: wo in der Kette; was du sehen kannst
Model answer
| Tool | Sees | Typical use |
|---|---|---|
| Servlet Filter | raw HTTP, runs even if no controller matches | Security, CORS, gzip |
| HandlerInterceptor | after mapping, has handler info | logging, MDC, simple authz |
@ControllerAdvice | controller exceptions / binding | API errors |
| AOP on services | method calls on Spring beans | transactions, audit |
Security is a filter chain before MVC. If Security rejects, your interceptor may never run.
Follow-ups
OncePerRequestFilter?- Why not do transactions in a filter?
Trap: putting business rules in a filter because “it runs on every request”.
Memory sentence: Filters wrap the servlet; interceptors wrap the handler; AOP wraps beans.
Study: MVC mental model
Musterantwort
| Mittel | Sieht | Typischer Einsatz |
|---|---|---|
| Servlet Filter | rohes HTTP, läuft auch ohne passenden Controller | Security, CORS, gzip |
| HandlerInterceptor | nach dem Mapping, kennt den Handler | Logging, MDC, einfache Authz |
@ControllerAdvice | Controller-Exceptions / Binding | API-Fehler |
| AOP auf Services | Methodenaufrufe auf Spring Beans | Transaktionen, Audit |
Security ist eine Filterkette vor MVC. Wenn Security ablehnt, läuft dein Interceptor vielleicht nie.
Nachfragen
OncePerRequestFilter?- Warum keine Transaktionen in einem Filter?
Falle: Business-Logik in einen Filter packen, weil „der läuft bei jedem Request“.
Merksatz: Filter wrappen das Servlet; Interceptors wrappen den Handler; AOP wrappt Beans.
Lesen: MVC-Mental-Model
10. Why DTOs instead of exposing entities?
Deutsch
Warum DTOs statt Entities nach außen geben?
Level: Mid · Listen for: lazy loads, over-posting, API stability
Niveau: Mid · Darauf hören sie: Lazy Loads, Over-Posting, API-Stabilität
Model answer
Entities are a persistence model: lazy proxies, bidirectional graphs, fields you never want to serialize (passwordHash). Returning them:
- triggers N+1 and
LazyInitializationException - leaks internals
- couples API clients to the database schema
- allows over-posting (client sets
role=ADMINon a user entity)
DTOs (records) are the HTTP contract. Map in the controller or a dedicated mapper. Persist entities only inside the service/repo layer.
Follow-ups
- MapStruct vs manual mapping?
- When is a sealed response type useful?
Trap: spring.jpa.open-in-view=true so you can serialize lazy fields.
Memory sentence: Entities belong to the database session; DTOs belong to the HTTP contract.
Study: JPA performance
Musterantwort
Entities sind ein Persistenz-Modell: lazy Proxies, bidirektionale Graphen, Felder, die du nie serialisieren willst (passwordHash). Wenn du sie zurückgibst:
- löst das N+1 und
LazyInitializationExceptionaus - gibt interne Details preis
- koppelt API-Clients ans Datenbankschema
- erlaubt Over-Posting (Client setzt
role=ADMINauf einer User-Entity)
DTOs (Records) sind der HTTP-Vertrag. Mappen im Controller oder einem eigenen Mapper. Entities nur in der Service-/Repo-Schicht persistieren.
Nachfragen
- MapStruct vs. manuelles Mapping?
- Wann ist ein
sealedResponse-Typ nützlich?
Falle: spring.jpa.open-in-view=true, damit du lazy Felder serialisieren kannst.
Merksatz: Entities gehören zur Datenbank-Session; DTOs gehören zum HTTP-Vertrag.
Lesen: JPA-Performance
11. How do you version a REST API?
Deutsch
Wie versionierst du eine REST-API?
Level: Senior · Listen for: compatibility, not fashion
Niveau: Senior · Darauf hören sie: Kompatibilität, keine Mode
Model answer
I prefer additive change for as long as possible: new fields, new endpoints. Breaking changes get a version.
Practical options:
- URI
/api/v1/orders— obvious, cache-friendly, a bit ugly - header
Accept: application/vnd.acme.v1+json— cleaner URIs, harder to try in a browser
I do not version every week. I do not keep v1, v2, v3, v4 in one controller with if-else. Separate types, or a separate application when the model truly diverged.
Follow-ups
- How long do you keep v1?
- Query-param
?version=downsides?
Trap: putting v1 in the package name of every class including entities.
Memory sentence: Compatible by default; version only when you must break.
Musterantwort
Ich bleibe so lange wie möglich bei additiven Änderungen: neue Felder, neue Endpunkte. Breaking Changes bekommen eine Version.
Praktische Optionen:
- URI
/api/v1/orders— offensichtlich, cache-freundlich, etwas hässlich - Header
Accept: application/vnd.acme.v1+json— sauberere URIs, im Browser schwerer auszuprobieren
Ich versioniere nicht jede Woche. Ich halte nicht v1, v2, v3, v4 in einem Controller mit if-else. Eigene Typen, oder eine eigene Anwendung, wenn das Modell wirklich auseinandergelaufen ist.
Nachfragen
- Wie lange behältst du v1?
- Nachteile von Query-Param
?version=?
Falle: v1 in den Package-Namen jeder Klasse packen, inklusive Entities.
Merksatz: Standardmäßig kompatibel; versionieren nur, wenn du brechen musst.
12. Pagination, filtering, and sorting without wrecking the DB?
Deutsch
Paginierung, Filter und Sortierung, ohne die DB zu ruinieren?
Level: Mid · Listen for: Pageable, stable order, max page size
Niveau: Mid · Darauf hören sie: Pageable, stabile Sortierung, max. Page Size
Model answer
I use Pageable (page, size, sort) with a max size and a default sort that is stable (id as a tie-breaker). Unstable sort + offset pagination duplicates or skips rows.
Offset pagination (LIMIT/OFFSET) is simple and degrades on large offsets. Keyset (seek) pagination is better for infinite scroll.
I never findAll() into a list and paginate in memory. Filters become indexed query parameters, not a JSON blob in a GET body.
Follow-ups
SlicevsPage? (Pagecounts total)- Why is
sort=passwordHasha problem?
Trap: allowing clients to sort by any entity field.
Memory sentence: Bound page size, force a stable sort, push filters into the query.
Study: Repository query methods
Musterantwort
Ich nutze Pageable (page, size, sort) mit einer maximalen size und einem stabilen Default-Sort (id als Stichentscheid). Instabiler Sort plus Offset-Pagination dupliziert oder überspringt Zeilen.
Offset-Pagination (LIMIT/OFFSET) ist einfach und wird bei großen Offsets schlecht. Keyset- (Seek-) Pagination ist besser für Endlos-Scrollen.
Ich lade nie per findAll() alles in eine Liste und paginiere dann im Speicher. Filter werden zu indizierten Query-Parametern, nicht zu einem JSON-Blob im GET-Body.
Nachfragen
Slicevs.Page? (Pagezählt die Gesamtzahl)- Warum ist
sort=passwordHashein Problem?
Falle: Clients nach jedem Entity-Feld sortieren lassen.
Merksatz: size begrenzen, stabilen Sort erzwingen, Filter in die Query schieben.
Lesen: Repository Query Methods
13. What is CORS, and who should handle it?
Deutsch
Was ist CORS, und wer soll es behandeln?
Level: Mid · Listen for: browser rule; Security config; not @CrossOrigin everywhere
Niveau: Mid · Darauf hören sie: Browser-Regel; Security-Config; nicht überall @CrossOrigin
Model answer
CORS is a browser protection: a page on origin A cannot read responses from origin B unless B opts in with headers. curl and server-to-server calls do not care.
In Boot I configure CORS in Spring Security (and MVC) with an explicit origin list. I do not @CrossOrigin(origins = "*") plus credentials.
Preflight is an OPTIONS request. If Security blocks OPTIONS, the SPA looks “broken” and people blame the API.
Follow-ups
Access-Control-Allow-Credentialsand*?- CSRF vs CORS? (different problems)
Trap: disabling CORS in prod to “make the frontend work”.
Memory sentence: CORS is for browsers; allow a list of origins in Security, not * with cookies.
Study: CSRF, CORS, JWT
Musterantwort
CORS ist ein Browser-Schutz: eine Seite auf Origin A darf Responses von Origin B nicht lesen, außer B erlaubt es per Header. Für curl und Server-zu-Server-Aufrufe ist das egal.
In Boot konfiguriere ich CORS in Spring Security (und MVC) mit einer expliziten Origin-Liste. Ich setze nicht @CrossOrigin(origins = "*") plus Credentials.
Preflight ist ein OPTIONS-Request. Wenn Security OPTIONS blockt, wirkt die SPA „kaputt“ und die Leute schieben es auf die API.
Nachfragen
Access-Control-Allow-Credentialsund*?- CSRF vs. CORS? (andere Probleme)
Falle: CORS in Prod abschalten, damit „das Frontend läuft“.
Merksatz: CORS ist für Browser; in Security eine Origin-Liste erlauben, nicht * mit Cookies.
Lesen: CSRF, CORS, JWT
14. How do you design a controller so it stays thin?
Deutsch
Wie designst du einen Controller, damit er dünn bleibt?
Level: Mid · Listen for: HTTP in, DTO out, no transactions in the controller
Niveau: Mid · Darauf hören sie: HTTP rein, DTO raus, keine Transaktionen im Controller
Model answer
The controller:
- maps HTTP to a command/query
- calls one service method
- maps the result to a DTO /
ResponseEntity - does not open transactions, does not call three repositories, does not contain business
ifs
Transactions, invariants, and multiple repositories belong in the service. Persistence details stay in repositories.
That also makes @WebMvcTest easy: mock the service, assert status and JSON.
Follow-ups
- What if two services must run in one transaction?
- GraphQL / async controllers?
Trap: @Transactional on the controller class.
Memory sentence: Controllers translate HTTP; services own the use case.
Study: Testing the web layer
Musterantwort
Der Controller:
- mappt HTTP auf Command/Query
- ruft eine Service-Methode auf
- mappt das Ergebnis auf ein DTO /
ResponseEntity - öffnet keine Transaktionen, ruft nicht drei Repositories auf, enthält keine fachlichen
ifs
Transaktionen, Invarianten und mehrere Repositories gehören in den Service. Persistenz-Details bleiben in Repositories.
Damit wird @WebMvcTest einfach: Service mocken, Status und JSON asserten.
Nachfragen
- Was, wenn zwei Services in einer Transaktion laufen müssen?
- GraphQL / async Controller?
Falle: @Transactional auf der Controller-Klasse.
Merksatz: Controller übersetzen HTTP; Services besitzen den Use Case.
Lesen: Web-Schicht testen