Skip to main content

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 HandlerInterceptor vs a servlet Filter?

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 HandlerInterceptor vs. 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 @Controller still return JSON? (yes, method-level @ResponseBody)
  • ResponseEntity vs 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 @Controller trotzdem JSON liefern? (ja, @ResponseBody auf der Methode)
  • ResponseEntity vs. 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
AnnotationSource
@PathVariableURI template /orders/{id}
@RequestParamquery string or form field
@RequestHeader / @CookieValueheaders / cookies
@RequestBodydecoded body (JSON via Jackson)
@ModelAttributeform 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 = false vs Optional<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

AnnotationQuelle
@PathVariableURI-Template /orders/{id}
@RequestParamQuery-String oder Form-Feld
@RequestHeader / @CookieValueHeader / Cookies
@RequestBodydekodierter Body (JSON über Jackson)
@ModelAttributeForm 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 = false vs. 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 ObjectMapper bean — does Boot back off?
  • @JsonIgnore vs 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?
  • @JsonIgnore vs. 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

  • @ExceptionHandler on 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

  • @ExceptionHandler auf 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 @Valid on a nested object not enough without @Valid on the nested field?
  • @Validated on 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 @Valid auf dem äußeren Objekt nicht ohne @Valid am verschachtelten Feld?
  • @Validated auf 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, Location header
  • 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
ToolSeesTypical use
Servlet Filterraw HTTP, runs even if no controller matchesSecurity, CORS, gzip
HandlerInterceptorafter mapping, has handler infologging, MDC, simple authz
@ControllerAdvicecontroller exceptions / bindingAPI errors
AOP on servicesmethod calls on Spring beanstransactions, 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

MittelSiehtTypischer Einsatz
Servlet Filterrohes HTTP, läuft auch ohne passenden ControllerSecurity, CORS, gzip
HandlerInterceptornach dem Mapping, kennt den HandlerLogging, MDC, einfache Authz
@ControllerAdviceController-Exceptions / BindingAPI-Fehler
AOP auf ServicesMethodenaufrufe auf Spring BeansTransaktionen, 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=ADMIN on 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 LazyInitializationException aus
  • gibt interne Details preis
  • koppelt API-Clients ans Datenbankschema
  • erlaubt Over-Posting (Client setzt role=ADMIN auf 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 sealed Response-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

  • Slice vs Page? (Page counts total)
  • Why is sort=passwordHash a 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

  • Slice vs. Page? (Page zählt die Gesamtzahl)
  • Warum ist sort=passwordHash ein 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-Credentials and *?
  • 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-Credentials und *?
  • 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