
Wir haben einen Marktplatz mit Spring-Boot-Microservices, zwei Next.js-Frontends und Stripe live gebracht. Der Shop war online, die Produkte waren da – angezeigt wurde: nichts. Eine Geschichte über ein einziges Wort im JSON, eine Lombok-Falle und ein Secret, das nie ins Git gehört hätte.
Der leere Shop
Es gibt diesen einen Moment, den jedes Team kennt, das schon mal etwas Echtes live gebracht hat. Das Backend läuft. Alle Tests grün. Das Deployment durch. Du öffnest den Shop, willst das erste Produkt bewundern – und siehst: nichts.
Kein Fehler. Kein Stacktrace. Einfach ein leerer Shop.
Wir haben in den letzten Wochen einen Marktplatz live gebracht – nennen wir ihn einen Lieferdienst für eine sehr analoge Branche. Unter der Haube: sechs Spring-Boot-Microservices (Gateway, Auth, drei Fach-Services, Service Discovery), zwei Next.js-Frontends, Postgres, Liquibase, Stripe, Datei-Uploads in die Cloud. Deployment auf Railway und Netlify. Klingt nach Lehrbuch. War es auch – nur stand in dem Lehrbuch nicht, was uns dann passiert ist.
Hier sind die fünf Lektionen, die uns niemand vorher erzählt hat. Damit sie dir erspart bleiben. Oder du wenigstens schneller lachst, wenn es dich trifft.
Lektion 1: Ein einziges Wort im JSON kann dein komplettes Frontend lahmlegen
Zurück zum leeren Shop. Die Produkte lagen in der Datenbank. Der Service lieferte sie aus. Das Frontend bekam sie. Und filterte sie alle weg.
Alle.
Der Grund war ein einziges Wort: Unser Java-Feld hieß isActive. Jackson, der JSON-Serializer von Spring, macht daraus nach JavaBeans-Konvention aber "active". Das Frontend prüfte brav auf product.isActive – und bekam undefined. undefined ist falsy. Und ein Shop, der nur aktive Produkte zeigt, zeigt bei lauter falsy-Produkten: gar nichts.
// Ein Wort Unterschied zwischen "läuft" und "leerer Shop":
@JsonProperty("isActive")
private boolean isActive;Der JSON-Contract ist heilig
Sobald Frontend und Backend in getrennten Repos leben, ist dein JSON-Contract heilig. Schreib ihn explizit hin – @JsonProperty, DTOs, von mir aus ein geteiltes Schema. Aber verlass dich nie darauf, dass zwei Frameworks dieselbe Konvention träumen.
Lektion 2: Lombok baut dir Objekte, die du so nie bestellt hast
Zweiter Klassiker, gleiche Woche. Neue Produkte wurden angelegt – und waren sofort inaktiv. Dabei stand im Code doch eindeutig:
private Boolean isActive = true; // sollte man meinenSollte man meinen. Wer Lomboks @Builder benutzt, bekommt diesen Default aber nicht. Der Builder ignoriert Feld-Initialisierer stillschweigend, solange man nicht @Builder.Default dranschreibt. Kein Compiler-Fehler, keine Warnung, einfach null beziehungsweise false – und dein frisch angelegtes Produkt ist unsichtbar.
@Builder.Default
private Boolean isActive = true; // jetzt stimmt'sWir haben daraus eine Team-Regel gemacht: Jedes Entity-Feld mit Initialisierer wird im Review auf @Builder.Default geprüft. Seitdem: Ruhe. Magische Annotations sparen dir hundert Zeilen Boilerplate – und kassieren dafür genau dann, wenn du nicht hinschaust.
Randnotiz, weil sie zu gut passt: Ein Kollege hat mir mal erklärt, Lombok sei – genau wie Java – eine Insel in Indonesien. Ich habe genickt und es innerlich unter „nette Theorie“ abgeheftet.
Er hatte recht. Hiermit hast du es schriftlich, Kollege.
Lombok liegt tatsächlich gleich hinter Bali, von Java aus einmal rechts abbiegen, und die Bibliothek ist nach der Insel benannt. Die Metapher liefert sie gratis mit: sieht aus wie das Paradies, hat aber Strömungen, die dich lautlos erwischen, wenn du unaufmerksam badest. Obendrein ist „lombok“ auf Indonesisch ein Wort für Chili. Schärfer als erwartet.
Kommt hin.
Lektion 3: „Läuft lokal“ ist keine Aussage über Produktion
Fünf von sechs Services starteten auf der Plattform sauber durch. Einer crashte beim Boot. Immer wieder. Derselbe Code-Stand, dieselbe Konfiguration, dieselbe Datenbank-Engine.
Die Ursache war fast schon comedy-reif: Genau dieser eine Service hatte als einziger die Liquibase-Dependency nicht in seiner pom.xml. Lokal fiel das nie auf, weil Hibernate die Tabellen nebenbei selbst anlegte. In Produktion – wo ddl-auto aus guten Gründen aus war – lief schlicht nie eine Migration. Der Service suchte Tabellen, die es nicht gab, und starb.
Lokal und Produktion müssen denselben Mechanismus benutzen
Wenn Migrationen dein Weg sind, dann überall – und ddl-auto gehört auch in der Entwicklung auf none, sonst testest du eine Welt, die es in echt nicht gibt. Bonus-Regel für Microservices: Wenn fünf Services etwas haben und einer nicht, ist das kein Feature. Das ist ein Bug mit Verspätung.
Lektion 4: Traue keinem Client. Auch nicht deinem eigenen.
Als die Bezahlung mit Stripe kam, haben wir einen Testkauf gemacht – und danach einen zweiten, bei dem wir per API einen Stückpreis von einem Cent mitgeschickt haben. Einfach mal gucken.
Der Server hat den Preis kommentarlos auf den echten Wert korrigiert. Puh.
Aber nur, weil wir es vorher genau so gebaut hatten: Der Client schickt Wünsche, der Server berechnet Wahrheiten. Preise, Staffeln, Optionsaufschläge – alles wird serverseitig gegen die Produktdaten neu gerechnet, der Client-Preis wird überschrieben. Dazu ein Webhook, der idempotent ist (zweimal dasselbe Event = einmal Wirkung) und Beträge, die als Snapshot an der Bestellung hängen, damit eine spätere Preisänderung keine alte Rechnung umschreibt.
Nichts davon ist Raketentechnik. Aber es ist die Sorte Arbeit, die man gern „später“ macht. Mach sie vor dem ersten echten Kunden. Der zweite API-Aufruf mit dem Ein-Cent-Preis kommt nicht von dir.
Lektion 5: Dein Git-Verlauf vergisst nichts – dein .gitignore sollte es wissen
Der unangenehmste Fund kam beim Aufräumen: eine .env im Repository. Getrackt. Mit einem produktiven API-Secret drin.
Wenn dir das passiert, reicht git rm --cached nicht. Die Historie kennt das Secret weiter – jeder Clone hat es. Es gibt genau eine saubere Antwort: rotieren. Neues Secret erzeugen, überall nachziehen, das alte ist damit wertlos. Fünfzehn Minuten Arbeit, wenn man weiß, dass man sie tun muss. Ein potenzielles Desaster, wenn nicht.
# Vorsorge kostet fünf Sekunden – vor dem ersten Push:
git ls-files | grep .env
# .env gehört vom ersten Commit an in die .gitignore.
# Secrets leben in der Plattform-Konfiguration
# (Railway, Netlify und Co. können das alle).Was bleibt
Man könnte aus alldem den falschen Schluss ziehen: Microservices, getrennte Repos, externe Payment-Provider – alles viel zu viele bewegliche Teile, lieber wieder Monolith und Feierabend.
Wir ziehen den gegenteiligen Schluss. Keine dieser Pannen lag an der Architektur. Sie lagen an impliziten Annahmen: dass Konventionen schon zusammenpassen, dass lokal wie Produktion ist, dass der Client schon ehrlich ist. Microservices haben diese Annahmen nicht erzeugt – sie haben sie sichtbar gemacht. Früher, als es ein Monolith getan hätte, und zu einem Zeitpunkt, an dem alles noch billig zu fixen war.
- JSON-Contract explizit machen: @JsonProperty, DTOs oder ein geteiltes Schema – nie auf Framework-Konventionen verlassen
- Lombok @Builder ignoriert Feld-Initialisierer – @Builder.Default ist Pflicht und gehört in die Review-Checkliste
- Lokal und Produktion brauchen denselben Migrations-Mechanismus – ddl-auto überall auf none
- Der Client schickt Wünsche, der Server berechnet Wahrheiten – Preise immer serverseitig neu rechnen
- Ein geleaktes Secret wird rotiert, nicht gelöscht – die Git-Historie vergisst nichts
Der Shop ist übrigens voll. Die Produkte sind aktiv, die Migrationen laufen, das Secret ist rotiert, und der Ein-Cent-Trick prallt am Server ab.
Und der nächste leere Shop? Kommt bestimmt. Aber dann wissen wir ja, wo wir zuerst hingucken: ins JSON.