Ein Ticket wird in einer Anwendung selten als einzelne Tabellenzeile behandelt. Wenn ein neues Ticket entsteht, braucht es oft zusätzlich einen ersten Kommentar, einen Ereigniseintrag und später einen Statuswechsel. Aus Sicht der Benutzerin ist das ein einziger fachlicher Vorgang. Aus Sicht der Datenbank sind es mehrere INSERT- und UPDATE-Anweisungen.
Genau an dieser Stelle werden Transaktionen im Anwendungscode wichtig. Eine Transaktion beantwortet nicht nur die Frage, ob PostgreSQL COMMIT oder ROLLBACK ausführt. Sie beantwortet vor allem die fachliche Frage: Welche Änderungen gehören so eng zusammen, dass sie nur gemeinsam dauerhaft gespeichert werden dürfen?
In Block 2 hast du db-2-app als Starterprojekt kennengelernt. Die Anwendung konnte Tickets lesen und erstellen. Für Block 3 bleibt der normale Lernpfad gleich: Du startest beim Starter und entwickelst weiter. Falls du mitten im Modul einsteigst, nutzt du den Checkpoint block-3-start. Das Checkpoint-System wird im separaten Abschnitt Checkpoints erklärt.
Warum Transaktionen in der Anwendung?¶
Transaktionen kennst du seit DB-1: Änderungen werden entweder dauerhaft gemacht oder verworfen. In DB-2 interessiert uns, wo diese Entscheidung im Anwendungscode liegt.
Betrachte einen einfachen Ticket-Workflow:
Ein Ticket wird erstellt.
Ein erster Kommentar wird gespeichert.
Ein Event
ticket_createdwird in den Verlauf geschrieben.
Wenn Schritt 2 oder 3 fehlschlägt, darf nicht einfach nur das Ticket übrig bleiben. Sonst zeigt die Anwendung ein Ticket ohne Verlauf oder ohne Startkommentar. Das ist technisch möglich, aber fachlich unvollständig.
Eine gute Transaktionsgrenze schützt den fachlichen Zusammenhang. Sie ist deshalb meistens nicht die Repository-Methode und nicht der Controller, sondern die Service-Methode, die den Ablauf koordiniert.
@Transactional im Service-Layer¶
Spring unterstützt deklarative Transaktionen mit @Transactional. Die Annotation wird typischerweise durch einen Spring-Proxy ausgewertet: Beim Eintritt in die Methode wird eine Transaktion geöffnet oder wiederverwendet, am Ende wird bei normalem Rücksprung committet und bei passenden Exceptions zurückgerollt VMware, Inc. (2026).
Im Kursprojekt liegt die Grenze bewusst im Service:
@Transactional
TicketResponse createTicket(CreateTicketRequest request) {
TicketEntity savedTicket = persistTicketWorkflow(request);
return ticketMapper.toResponse(savedTicket);
}Die Schichten haben dabei unterschiedliche Rollen:
| Schicht | Rolle im Transaktionskontext |
|---|---|
| Controller | HTTP-Daten entgegennehmen, validieren und an den Service delegieren |
| Service | fachlichen Ablauf und Transaktionsgrenze festlegen |
| Repository | einzelne Datenbankoperationen kapseln |
| PostgreSQL | Constraints, Fremdschlüssel und dauerhafte Speicherung erzwingen |
Der Service ist der passende Ort, weil er weiss, welche Schritte fachlich zusammengehören. Das Repository weiss nur, wie eine Entity gespeichert wird. Der Controller sollte nicht entscheiden, welche Datenbankänderungen atomar sein müssen.
Commit und Rollback lesen¶
In Spring gilt als Grundregel: Eine RuntimeException oder ein Error löst bei @Transactional standardmässig ein Rollback aus. Checked Exceptions werden nicht automatisch gleich behandelt, wenn nichts anderes konfiguriert ist VMware, Inc. (2026).
Für den Unterricht reicht diese Faustregel:
Wenn die Methode normal endet, wird committet.
Wenn eine unbehandelte Runtime Exception aus der Methode herausläuft, wird zurückgerollt.
Wenn ein Fehler abgefangen und verschluckt wird, sieht Spring am Methodenende keinen Rollback-Grund.
Das ist der Grund, warum Fehlerbehandlung nicht nur Stilfrage ist. Eine Service-Methode, die einen Fehler abfängt, loggt und trotzdem normal zurückkehrt, kann einen unvollständigen Vorgang committen.
@Transactional
void falsch() {
ticketRepository.save(ticket);
try {
eventRepository.save(event);
} catch (RuntimeException ex) {
// Problem: Methode laeuft danach normal weiter.
}
}Besser ist, den Fehler entweder bewusst weiterzugeben oder eine fachliche Alternative zu implementieren, die keinen halben Zustand hinterlässt.
Fachliche Invarianten¶
Eine Invariante ist eine Regel, die nach jedem erfolgreichen Vorgang gelten muss. Im Ticket-System können solche Regeln sein:
Ein neu erstelltes Ticket hat einen gültigen Status.
Ein Ticket mit Startkommentar hat einen Kommentar-Eintrag, der auf das Ticket zeigt.
Jeder Statuswechsel erzeugt ein Event.
Ein geschlossenes Ticket wird in diesem einfachen Modell nicht wieder geöffnet.
Manche Regeln gehören in PostgreSQL:
ALTER TABLE app_starter.tickets
ADD CONSTRAINT tickets_status_check
CHECK (status IN ('open', 'waiting', 'closed'));Andere Regeln brauchen Service-Logik:
private void assertStatusTransition(String oldStatus, String newStatus) {
if ("closed".equals(oldStatus)) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Geschlossene Tickets werden nicht wieder geoeffnet"
);
}
}Beides ergänzt sich. PostgreSQL schützt zentrale Datenregeln unabhängig vom Schreibpfad. Der Service schützt fachliche Abläufe, die mehr Kontext brauchen als eine einzelne Spalte.
Externe Seiteneffekte¶
Eine Datenbanktransaktion kann Datenbankänderungen zurückrollen. Sie kann aber keine bereits gesendete E-Mail zurückholen, keinen HTTP Call ungeschehen machen und keine Message aus einer externen Queue entfernen.
Riskant ist deshalb ein Ablauf wie:
Transaktion beginnt
Ticket speichern
E-Mail senden
Event speichern schlaegt fehl
RollbackNach dem Rollback existiert das Ticket nicht, aber die E-Mail wurde schon verschickt. Das ist kein Fehler von PostgreSQL. Es ist eine falsch gewählte Grenze zwischen Datenbanktransaktion und Systemprozess.
Für Block 3 genügt die Grundentscheidung:
Datenbankänderungen, die fachlich zusammengehören, liegen in einer kurzen Transaktion.
Externe Effekte werden bewusst danach ausgelöst oder in einem eigenen Muster behandelt.
Im Code Review wird geprüft, ob externe Effekte mitten in der Transaktion stehen.
Wir vertiefen hier keine Event-Architektur. Entscheidend ist, dass du die Grenze erkennst.
Optimistic Locking¶
Nebenläufigkeit wird sichtbar, wenn zwei Personen dasselbe Ticket bearbeiten. Beide lesen den Status open. Person A setzt auf waiting, Person B setzt fast gleichzeitig auf closed. Ohne Schutz kann die spätere Speicherung die frühere Änderung überschreiben.
Optimistic Locking löst dieses Problem mit einer Versionsspalte. Die Entity erhält ein Feld mit @Version; Hibernate/JPA nutzt dieses Feld, um beim Speichern zu prüfen, ob der gelesene Stand noch aktuell ist Hibernate Team (2026).
@Version
@Column(name = "version")
private Long version;Die Datenbankmigration ergänzt die passende Spalte:
ALTER TABLE app_starter.tickets
ADD COLUMN version BIGINT NOT NULL DEFAULT 0;Die Idee ist bewusst einfach:
Optimistic Locking verhindert nicht, dass zwei Personen gleichzeitig arbeiten. Es verhindert, dass eine Anwendung unbemerkt auf veraltetem Stand speichert. Die fachliche Reaktion bleibt Aufgabe der Anwendung: neu laden, Konflikt melden oder Benutzerin entscheiden lassen.
Review-Fragen für Block 3¶
Beim Lesen eines Transaktionsablaufs helfen diese Fragen:
Welcher fachliche Vorgang wird geschützt?
Beginnt und endet die Transaktion an der richtigen Stelle?
Welche Tabellenänderungen müssen gemeinsam committen?
Welche Exception würde ein Rollback auslösen?
Werden Fehler versehentlich abgefangen?
Gibt es externe Seiteneffekte innerhalb der Transaktion?
Braucht der Vorgang Schutz gegen gleichzeitige Bearbeitung?
Eine gute Antwort nennt nicht nur @Transactional. Sie begründet, warum genau diese Grenze den fachlichen Zustand schützt.
- VMware, Inc. (2026). Using @Transactional. In Spring Framework Reference Documentation. https://docs.spring.io/spring-framework/reference/7.0/data-access/transaction/declarative/annotations.html
- Hibernate Team. (2026). Hibernate ORM User Guide. https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html