21 Die Schichtenarchitektur

Spring-Anwendungen folgen einem bewährten Muster: Schichten. Jede Schicht hat eine klare Verantwortung. Keine Schicht weiß zu viel über die anderen. Das Ergebnis ist wartbarer, testbarer Code.

21.1 Die drei Schichten

Web-Schicht: Nimmt Anfragen entgegen, validiert Eingaben, formatiert Ausgaben. Kennt HTTP, kennt keine Geschäftslogik.

Service-Schicht: Implementiert die Geschäftslogik. Kennt die Fachdomäne, kennt kein HTTP.

Daten-Schicht: Speichert und lädt Daten. Kennt die Datenbank, kennt keine Geschäftsregeln.

21.2 Im Arcade-Projekt

Das Arcade-Projekt zeigt diese Struktur:

de.digitalfrontiers.arcade/
├── web/                    <- Web-Schicht
│   ├── HomeController
│   └── GameApiController
├── service/                <- Service-Schicht
│   └── GameService
└── repository/             <- Daten-Schicht
    └── InMemoryGameRepository

Der Request-Fluss für “Zeige alle Spiele”:

Jede Komponente macht nur ihren Job. Der Controller weiß nicht, woher die Spiele kommen. Der Service weiß nicht, dass es einen HTTP-Request gab. Das Repository weiß nicht, wofür die Daten verwendet werden.

21.3 Warum Schichten?

Testbarkeit: Jede Schicht ist isoliert testbar. Service-Tests brauchen keinen HTTP-Server. Repository-Tests brauchen keine Geschäftslogik.

Austauschbarkeit: Die Datenbank wechseln? Nur die Repository-Schicht betroffen. REST statt HTML? Nur die Web-Schicht betroffen.

Teamarbeit: Frontend-Entwickler arbeiten an Controllern. Backend-Entwickler an Services. Datenbank-Experten an Repositories. Klare Grenzen, wenig Konflikte.

Wartbarkeit: Bugs lassen sich eingrenzen. “Die Berechnung ist falsch” → Service-Schicht. “Die Anzeige ist falsch” → Web-Schicht. “Die Daten fehlen” → Daten-Schicht.

21.4 Die Regeln

Die Abhängigkeiten zeigen immer nach unten. Nie nach oben, nie quer.

Erlaubt: - Controller → Service → Repository

Verboten: - Controller → Repository (überspringt Service) - Service → Controller (falsche Richtung) - Repository → Service (falsche Richtung)

21.5 Was in welche Schicht gehört

Aufgabe Schicht Beispiel
Request parsen Web @RequestParam, @PathVariable
Validierung (Format) Web @Valid, @NotNull
Response formatieren Web JSON, HTML, Status Codes
Geschäftsregeln Service “Highscore nur wenn > bisheriger”
Transaktionen Service @Transactional
Orchestrierung Service Mehrere Repos kombinieren
CRUD-Operationen Repository save(), findById(), delete()
Queries Repository findByGameIdOrderByPoints()

21.6 Ein Anti-Pattern

Manchmal sieht man das:

// SCHLECHT: Controller macht zu viel
@RestController
public class GameController {
    
    @Autowired
    private GameRepository repository;  // Direkt das Repository!
    
    @GetMapping("/api/games/{id}")
    public Game getGame(@PathVariable String id) {
        Game game = repository.findById(id).orElse(null);
        if (game == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        // Geschäftslogik im Controller...
        if (game.isArchived()) {
            throw new ResponseStatusException(HttpStatus.GONE);
        }
        return game;
    }
}

Das funktioniert, verletzt aber die Schichtentrennung. Geschäftslogik (isArchived()-Prüfung) gehört in den Service. Der Controller sollte nur delegieren:

// BESSER: Controller delegiert
@RestController
public class GameController {
    
    private final GameService gameService;
    
    @GetMapping("/api/games/{id}")
    public Game getGame(@PathVariable String id) {
        return gameService.getActiveGame(id);
    }
}

@Service
public class GameService {
    
    public Game getActiveGame(String id) {
        Game game = repository.findById(id)
            .orElseThrow(() -> new GameNotFoundException(id));
        if (game.isArchived()) {
            throw new GameArchivedException(id);
        }
        return game;
    }
}

Die Logik ist jetzt wiederverwendbar. Ein anderer Controller, ein Batch-Job, ein CLI-Tool – alle können getActiveGame() nutzen.

21.7 Schichten sind keine Bürokratie

Für triviale CRUD-Anwendungen wirken Schichten wie Overhead. “Ich will doch nur Daten laden!” Ja, aber:

Die Schichten sind eine Investition in die Zukunft. Sie kosten wenig und zahlen sich aus, sobald die Anwendung wächst.