Coding Guidelines

Zielgruppe: Entwickler:in. Diese Regeln gelten für den gesamten Reactor und werden teilweise automatisiert erzwungen (ArchUnit, siehe architecture-tests) - kein reines Lippenbekenntnis.

Grundprinzipien

  1. Konstruktor-Injection, keine Feld-Injection. Erzwungen durch ConstructorInjectionTest (ArchUnit). Begründung: sichtbare, final-fähige, ohne Spring-Kontext testbare Abhängigkeiten (siehe Pattern-Katalog).
  2. common-domain bleibt framework-frei. Kein @Entity, kein @Autowired, kein org.springframework.*/jakarta.*-Import in diesem Modul - erzwungen durch CommonDomainPurityTest. Domainlogik muss sich in einem reinen JUnit-Test ohne Container verifizieren lassen.
  3. Java-Records statt Lombok. Value Objects und DTOs werden als record mit validierendem kompaktem Konstruktor umgesetzt (siehe Isbn, Money, EmailAddress). Kein Lombok im gesamten Reactor (Begründung siehe Tech-Stack-Katalog).
  4. Fehler zentral behandeln, nicht lokal wegfangen. Fachliche Fehler werfen eine DomainException-Subklasse; das Mapping auf HTTP-Status/RFC-7807 passiert EINMAL zentral in GlobalExceptionHandler (aus common-web). Kein Controller fängt DomainException selbst ab.
  5. Jeder Service bleibt für seine Daten allein zuständig (Database-per-Service, ADR-0002). Ein Service liest/schreibt niemals direkt die Datenbank eines anderen Service - Zugriff nur über dessen REST-API oder über Events.

Paketstruktur je Fachservice (ab Phase 2)

Jeder Fachservice (catalog-service, member-service, ...) folgt einer einheitlichen, schichtenorientierten Paketstruktur unterhalb von com.seb4u.library.<service>:

<service>/
├── domain/            Aggregate, Value Objects, Domain Events, Repository-INTERFACES
│                       (kennt keine Persistenz-/Web-Technologie)
├── application/        Application Services (Anwendungsfälle), orchestrieren das Domainmodell,
│                       lösen Domain Events in Integrationsevents auf
├── api/                REST-Controller, DTOs, OpenAPI-Annotationen
├── infrastructure/
│   ├── persistence/    JPA-Entities, Spring-Data-Repositories, Mapper Domain <-> Entity
│   └── messaging/       Kafka-/RabbitMQ-Producer und -Consumer
└── config/             Spring-`@Configuration`-Klassen

Abhängigkeitsrichtung (wichtig!): apiapplicationdomaininfrastructure. Das Domainpaket hat KEINE Abhängigkeit nach außen (siehe common-domain-Regel oben, dieselbe Idee gilt auch service-intern für das jeweilige domain-Paket). Diese Regel wird per ArchUnit erzwungen (architecture-tests/.../LayeredArchitectureTest, seit Phase 2 aktiv für member-service).

Bewusste Ausnahme: api darf einfache domain-Typen ohne Framework-Kopplung (Value Objects, Enums, IDs - z. B. MembershipTier, MemberId) direkt in DTOs/Controllern verwenden, statt sie pro Schicht neu zu definieren. Fachliche Abläufe müssen trotzdem immer über application laufen - api darf niemals direkt eine Methode auf einem Repository oder einem Aggregate aufrufen, die einen Zustand verändert. infrastructure wird von KEINER anderen Schicht direkt referenziert (nur über das in domain definierte Repository-Interface).

Zweite Ausnahme (event-getriebene Services - reservation-/fine-/notification-service): dort darf infrastructure (genauer: die Kafka-/RabbitMQ-Listener) application aufrufen. Ein Message-Listener ist architektonisch ein TREIBENDER Adapter - er spielt dieselbe Rolle wie ein Controller (er löst einen Anwendungsfall aus), auch wenn er technisch im Infrastructure-Paket liegt (weil er Kafka/RabbitMQ kennt). Diese Regel ist nicht willkürlich, sondern entstand aus einem echten ArchUnit-Regelverstoß beim ersten Testlauf dieser Services - siehe architecture-tests/.../LayeredArchitectureTest#checkEventDrivenLayering-Javadoc für die vollständige Herleitung.

Stolperstein: mvn spring-boot:run verdeckt ein kaputtes mvn package-Jar

Gefunden in Phase 7 beim ersten podman build + java -jar (siehe library-platform/Dockerfile): mvn package erzeugte ein Jar, das mit no main manifest attribute abstürzte, obwohl der komplette Reactor seit Phase 1 fehlerfrei baute und alle Services in der IDE über mvn spring-boot:run liefen.

Ursache: Der Goal repackage von spring-boot-maven-plugin (macht aus dem normalen Jar ein ausführbares Spring-Boot-Jar mit Main-Class) bindet sich nur dann automatisch an die package-Phase, wenn spring-boot-starter-parent der Parent-POM ist. Dieses Projekt nutzt bewusst einen eigenen Parent (library-platform-parent, siehe ADR zur BOM-Struktur) - ohne explizite <executions> lief repackage deshalb nie, in keinem der neun Services.

Warum es so lange unbemerkt blieb: mvn spring-boot:run startet direkt aus den kompilierten .class-Dateien und braucht das gepackte Jar gar nicht - der Fehler zeigt sich ausschließlich beim Ausführen des tatsächlichen mvn package-Artefakts (java -jar target/*.jar), was vor Phase 7 (Dockerfiles) schlicht niemand getan hatte.

Lösung: <executions> mit Goal repackage zentral in der pluginManagement des Parent-POM ergänzt (siehe library-platform/pom.xml) - gilt seither automatisch für alle Services.

Direkter Folgefehler, sofort danach gefunden: Ohne <classifier> ERSETZT repackage das normale dünne Jar durch das ausführbare fette Jar unter demselben Dateinamen (Klassen wandern dabei nach BOOT-INF/classes/). architecture-tests hängt aber per normaler Maven-<dependency> von den Fachservices ab, um deren kompilierte Klassen mit ArchUnit einzulesen - mit der BOOT-INF/-Struktur fand ArchUnit dort plötzlich gar nichts mehr (Layer 'Domain' is empty für alle vier Schichten, obwohl der Code unverändert war). Fix: <classifier>exec</classifier> auf der repackage-Execution - das dünne Jar bleibt unter dem alten Namen bestehen (architecture-tests bemerkt nichts), das ausführbare Jar heißt zusätzlich *-exec.jar (siehe library-platform/Dockerfile). Lehre: ein Fix kann einen zweiten, subtileren Fehler an unerwarteter Stelle auslösen - nach JEDER POM-Änderung den kompletten Reactor bauen, nicht nur das direkt betroffene Modul.

Lehre für Reviewer:innen: "Der Reactor baut grün" (mvn verify) ist NICHT dasselbe wie "das Artefakt ist lauffähig" - ein PR, der ein neues Deployment-Artefakt einführt (Container-Image, Helm-Chart, ...), sollte immer mindestens einmal podman build + podman run (oder Äquivalent) tatsächlich ausführen, bevor er als geprüft gilt (siehe PR-Checkliste).

Naming Conventions

Wie ein neues Feature hinzufügen? (Kochrezept)

Wird ab Phase 2 an einem konkreten Beispiel (z. B. "Buch als verloren melden") durchexerziert - bis dahin gilt als Faustregel: von innen nach außen entwickeln (zuerst Domainmodell + Unit-Test, dann Application Service, dann Persistenz-Adapter, zuletzt REST-Endpunkt) statt "von außen nach innen" (Controller zuerst) - letzteres verleitet dazu, Geschäftslogik direkt im Controller unterzubringen.

⌂ Cockpit