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
- Konstruktor-Injection, keine Feld-Injection. Erzwungen durch
ConstructorInjectionTest(ArchUnit). Begründung: sichtbare,final-fähige, ohne Spring-Kontext testbare Abhängigkeiten (siehe Pattern-Katalog). common-domainbleibt framework-frei. Kein@Entity, kein@Autowired, keinorg.springframework.*/jakarta.*-Import in diesem Modul - erzwungen durchCommonDomainPurityTest. Domainlogik muss sich in einem reinen JUnit-Test ohne Container verifizieren lassen.- Java-Records statt Lombok. Value Objects und DTOs werden als
recordmit validierendem kompaktem Konstruktor umgesetzt (sieheIsbn,Money,EmailAddress). Kein Lombok im gesamten Reactor (Begründung siehe Tech-Stack-Katalog). - Fehler zentral behandeln, nicht lokal wegfangen. Fachliche Fehler werfen eine
DomainException-Subklasse; das Mapping auf HTTP-Status/RFC-7807 passiert EINMAL zentral inGlobalExceptionHandler(auscommon-web). Kein Controller fängtDomainExceptionselbst ab. - 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!): api → application → domain ← infrastructure.
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
- Domain Events: Vergangenheitsform,
<Aggregat><WasPassiertIst>Event(z. B.LoanCreatedEvent). - Commands: Imperativ,
<TuDies>Command(z. B.SendNotificationCommand). - Application Services:
<Anwendungsfall>Serviceoder<Anwendungsfall>UseCase(wird pro Service konsistent gehalten, siehe jeweilige Modul-Doku ab Phase 2). - REST-DTOs:
<Resource>Request/<Resource>Response, nie dieselbe Klasse wie die JPA-Entity oder das Domainmodell (Vermeidung von "Leaky Abstractions" zwischen Schichten).
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.