1. Pakete als „Landkarte“ des Projekts
Pakete in Java werden oft als etwas Kosmetisches angesehen: „Nur Ordner, damit die Dateien nicht im Weg sind.“ Aber in einem echten Backend‑Projekt ist ein Paket fast wie eine Straße und Hausnummer: An der Adresse versteht man, wo der Code lebt und wofür man ihn braucht. Eine gute Struktur hilft bei der Orientierung, ohne jede Datei lesen zu müssen.
Stellen Sie sich vor, Sie haben ein Apartment‑Studio, in dem gleichzeitig Küche, Schlafzimmer, Abstellkammer, Büro und Fitnessstudio untergebracht sind. Technisch gesehen kann man darin leben. Aber Socken zwischen Töpfen und Hanteln zu suchen, ist ein zweifelhaftes Vergnügen. Ein flaches Java‑Projekt „in einem Paket“ ist ungefähr dasselbe: Die Klassen existieren physisch, aber die Navigation und das Verständnis der Rollen brechen bereits bei 15–20 Dateien auseinander.
In Spring Boot ist das noch wichtiger als in „einfachem Java“, weil Pakete nicht nur die Lesbarkeit beeinflussen, sondern auch das, was der Container überhaupt sieht. Das heißt, die Struktur ist nicht nur Ästhetik, sondern auch Teil der Mechanik der Anwendung. Und außerdem ist es Disziplin für das Gehirn: Wenn Sie ein Paket wählen, sind Sie gezwungen, sich die einfache Frage zu beantworten: „Dieser Code um was? Um die Fachdomäne? Um die Infrastruktur? Um die Diagnostik?“
Und hier kommt der Kerngedanke des Tages: Pakete sollten helfen, das Projekt einer Person zu erklären, die das Repository zum ersten Mal öffnet. Idealerweise könnte diese Person in zwei Wochen Sie selbst sein – denn das Gedächtnis eines Programmierers ist wie ein Browser‑Cache: Der Cache verfällt, aber die Bugs bleiben.
2. Das oberste Paket und Component Scanning
Jetzt kommt ein wichtiger Punkt, der die Angewohnheit „Ach, ich lege die main‑Klasse einfach irgendwo hin, sie startet doch nur“ bricht. In Spring Boot ist die main‑Klasse nicht einfach public static void main(...). Sie legt den Aufbaupunkt der Anwendung fest, denn @SpringBootApplication beinhaltet das Component Scanning, und die Scan‑Suche beginnt in dem Paket, in dem diese Klasse liegt.
Einfach gesagt: Boot schaut auf das Paket com.example.catalogservice und sagt: „Okay, ich werde nach Komponenten in diesem Paket und in allen Unterpaketen suchen.“ Daher ist es vorteilhaft, die Hauptklasse möglichst weit „oben“ im Wurzelpaket der Anwendung zu halten, damit sie alles „sieht“, was benötigt wird.
Hier ist eine gute, „standardmäßig korrekte“ Variante:
package com.example.catalogservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// Haupteingangspunkt: Vom Paket dieser Klasse aus startet Spring Boot das Component Scanning
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Starten des Spring-Kontexts und des gesamten Lebenszyklus der Anwendung
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
Und hier ist eine typische Anfängerfalle: Sie legen die main‑Klasse z. B. in com.example.catalogservice.config, weil „das ist doch die Konfiguration der Anwendung“. Aber dann stellt sich plötzlich heraus, dass com.example.catalogservice.catalog.* nicht mehr gescannt wird und Ihre @Service/@Repository einfach nicht erstellt werden. Und Sie beginnen, eine Verschwörung zu vermuten, den rückläufigen Merkur und Kotlin – obwohl der Grund banal ist: Sie haben selbst den Scan‑Bereich beschränkt.
Es ist sehr praktisch, sich folgendes Bild im Kopf zu behalten (nicht über die Interna von Spring, sondern über den gesunden Menschenverstand):
flowchart TD
A["com.example.catalogservice CatalogServiceApplication"] --> B["config"]
A --> C["catalog"]
A --> D["support"]
A --> E["actuator"]
C --> C1["domain"]
C --> C2["repository"]
C --> C3["service"]
C --> C4["web"]
C --> C5["bootstrap"]
Das Scannen erfolgt „den Baum hinunter“. Deshalb sollte der Baum so aufgebaut sein, dass die Wurzel dort ist, wo Sie erwarten, „das gesamte Projekt zu sehen“.
3. Hybrid aus Feature und Infrastruktur
Wenn Sie zum ersten Mal versuchen, „es schön zu machen“, besteht die Versuchung, ins Extrem zu gehen. Manche machen eine „Schichten“-Struktur (controller/service/repository), manche eine „Feature“-Struktur (catalog, orders, users), und manche machen ein drittes Extrem – das Paket util, in das sie alles stecken, was kein Zuhause gefunden hat. In unserem Kurs wählen wir eine hybride, sehr praktische Variante.
Der Schichtenansatz (web, service, repository) ist gut, wenn das Projekt sehr klein ist und Sie nur eine Fachdomäne haben. Aber sobald eine zweite Funktion hinzukommt, erhalten Sie eine lange Liste von Controllern in einem Paket und eine lange Liste von Services in einem anderen, und aus der Struktur wird nicht klar, welche Klassen miteinander zusammenhängen.
Der Feature‑Ansatz (jede Funktion lebt als separater Block und hat darin ihre eigenen web/service/repository) skaliert hervorragend, kann aber für ein Studienprojekt am Anfang wie eine Überkomplizierung wirken: „Warum brauche ich Modularität, wenn ich nur eine Domäne habe?“
Daher machen wir es für catalog-service so: eine Haupt‑Feature‑Zone – das Paket catalog – und daneben Infrastrukturpakete, die nicht zu einer bestimmten Geschäftsfunktion gehören, aber zur Anwendung als Plattform: config, support, actuator. So liest sich das Projekt sehr natürlich: „Hier ist die Fachdomäne, und hier ist die Umgebung drum herum.“
Und ja, das ist auch pädagogisch ehrlich. Wir tun nicht so, als ob wir ein riesiges Enterprise bauen, aber wir machen auch nicht „einen Haufen Klassen an einem Ort“. Wir bauen eine Struktur, die das Wachstum des Kurses aushält und nicht am Tag 10 auseinanderfällt, wenn die Web‑Schicht hinzukommt.
4. Struktur des catalog-service
Damit die Struktur keine „abstrakte Idee“ bleibt, muss sie als erwartete Karte des Projekts festgehalten werden. In diesem Kurs muss die Karte nicht für alle Zeiten perfekt sein, aber sie sollte stabil genug sein, damit Sie sich bis zum Ende des Kurses daran orientieren können. Daher legen wir vorab den Zielbaum der Pakete fest und füllen ihn nach und nach mit Klassen.
Hier ist das „Skelett“, das wir anstreben (Java‑Pakete und Klassen):
com.example.catalogservice ├── CatalogServiceApplication ├── config/ ├── catalog/ │ ├── domain/ │ ├── repository/ │ ├── service/ │ ├── bootstrap/ │ └── web/ ├── actuator/ └── support/
Das Paket catalog.web legen wir jetzt schon als Platz für den eingehenden Web‑Adapter an. Das ist noch kein Gespräch über die MVC‑Mechanik, sondern einfach eine strukturelle Grenze: Controller werden später nicht „irgendwo“ landen.
Und es ist wichtig, daran zu denken: Im Projekt gibt es nicht nur src/main/java, sondern auch src/main/resources. In resources befinden sich keine Java‑Pakete, sondern Ressourcendateien: Konfiguration, statische Seiten, Bilder usw. Wenn Sie src/main/resources/static sehen, ist das kein „Paket static“, sondern ein Ordner für statische Web‑Ressourcen. Wir greifen dort noch nicht ein, aber es ist schon jetzt nützlich, diese beiden Welten nicht zu verwechseln.
Um zu festigen, „wer wofür verantwortlich ist“, ist es praktisch, eine kleine Tabelle zu haben. Sie ersetzt nicht das Lesen des Codes, hilft aber, schnell die Rolle jedes Blocks zu rekapitulieren:
| Paket | Betrifft… | Beispiele für das, was dort lebt |
|---|---|---|
| catalog | die Fachdomäne „Kurskatalog“ | Modelle, Repository, Services, Bootstrap‑Code des Katalogs |
| config | explizite Java‑Konfiguration der Anwendung | @Configuration‑Klassen, @Bean‑Methoden |
| support | kleine Infrastrukturhelfer | Normalisierer, kleine Hilfsfunktionen mit klarer Rolle |
| actuator | Diagnostik und Health‑Check‑Logik | Health‑Indikatoren und andere Diagnostik‑Dinge |
| resources/static | statische Ressourcen für das Web | index.html und alles, was als Dateien ausgeliefert wird |
Der Sinn ist, dass Sie das Projekt öffnen und sofort verstehen können: „Okay, die Geschäftslogik ist hier, die Konfiguration dort, und der Rest sind Hilfszonen.“
5. Feature‑Zone catalog
Das Paket catalog ist unsere „Welt der Fachdomäne“. Es muss kein perfektes DDD‑Paradies sein, aber es muss verständlich sein: Hier liegt alles, was zum Kurskatalog gehört, und nicht dazu, wie genau Spring Boot das startet. Wir halten catalog bewusst als separaten Block, um Domäne und Infrastruktur nicht zu vermischen.
Innerhalb von catalog teilen wir den Code vorab nach Rollen auf, weil die Rollen der Klassen unterschiedlich sind. Modelle (domain) beschreiben Daten und Konzepte. Das Repository (repository) ist für den Datenzugriff zuständig (in unserem Projekt In‑Memory, ohne Datenbank). Der Service (service) enthält die Anwendungslogik und Regeln. Bootstrap (bootstrap) sind Start‑Dinge, die mit dem Katalog zusammenhängen (Initialisierung, Seeding, Startbericht). Web (web) ist der Platz für den eingehenden Web‑Adapter, damit die Controller neben dem Katalog leben und nicht in zufälligen Paketen auftauchen.
Schauen wir uns an, wie eine „normale“ Domänenklasse im richtigen Paket aussieht. Sie muss nichts von Spring wissen – und das ist ein Plus, kein Minus:
package com.example.catalogservice.catalog.domain;
public class CourseCard {
// Identifikator des Kurses in URL/Routen (vorerst nur eine Demonstration des Domänenfelds)
private final String slug;
public CourseCard(String slug) {
// Im Domänenmodell können später Invarianten/Prüfungen untergebracht werden; jetzt fixieren wir die Struktur
this.slug = slug;
}
}
Hier kommt es nicht auf die „Dürftigkeit des Modells“ an (wir werden es später erweitern), sondern darauf, dass am Dateipfad und am Paket sichtbar ist: Dies ist ein Domänenmodell des Katalogs, keine Konfiguration und kein Controller.
Jetzt ein Beispiel für einen Service. Er ist bereits eine Spring‑Komponente, daher mit @Service markiert, und wie in der letzten Lektion besprochen, kommen Abhängigkeiten über den Konstruktor:
package com.example.catalogservice.catalog.service;
import com.example.catalogservice.catalog.repository.CourseCatalogRepository;
import org.springframework.stereotype.Service;
@Service
public class CourseCatalogService {
// Der Service stützt sich auf das Repository für den Zugriff auf die Katalogdaten
private final CourseCatalogRepository repository;
public CourseCatalogService(CourseCatalogRepository repository) {
// Konstruktorinjektion: Abhängigkeit ist obligatorisch und explizit sichtbar
this.repository = repository;
}
}
Beachten Sie, wie sich die Struktur liest: catalog.service hängt von catalog.repository ab. Selbst ohne Details zu kennen, können Sie bereits vermuten, dass der Service Kurse aus dem Repository holt und Anwendungsaktionen ausführt.
Und hier ein minimales Beispiel für ein Repository‑Interface – ebenfalls nicht um der Logik willen, sondern zur Demonstration des Platzes in der Struktur:
package com.example.catalogservice.catalog.repository;
import com.example.catalogservice.catalog.domain.CourseCard;
import java.util.List;
public interface CourseCatalogRepository {
// Vertrag des Repository: Gibt alle Kurskarten zurück (Implementierung folgt später)
List<CourseCard> findAll();
}
Bisher besprechen wir keine vollständige Web‑Schicht und keinen API‑Vertrag, daher kann catalog.web leer sein – das ist in Ordnung. Wichtig ist, dass der Platz für den eingehenden Adapter bereits vorhanden ist und die Controller später nicht „irgendwo“ landen. Die Angewohnheit „irgendwo“ ist der Hauptlieferant zukünftiger Refactorings.
6. Infrastrukturpakete
Schauen wir uns nun an, was sich neben catalog im Wurzelverzeichnis der Anwendung befindet. Das sind die Infrastrukturzonen. Sie sind nicht dafür zuständig, „welche Kurse wir haben“, sondern dafür, „wie die Anwendung aufgebaut ist“, „wie sie startet“, „wie man sie erweitert und diagnostiziert“. Es ist praktisch, sie getrennt zu halten, damit die Domäne nicht in der Umgebung untergeht.
Das Paket config ist der Ort für die Java‑Konfiguration. Dort legen wir Klassen mit @Configuration ab, um das Projekt nicht in eine Müllhalde von Annotationen im gesamten Code zu verwandeln. Und ja, die Konfiguration kann am Anfang leer sein, aber allein die Tatsache, dass sie in config lebt, diszipliniert.
Hier ist ein minimales Beispiel einer Konfigurationsklasse (momentan tut sie nichts, zeigt aber Ort und Rolle):
package com.example.catalogservice.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StartupConfiguration {
// Hier werden später @Bean-Methoden und Wiring-Einstellungen auftauchen, falls nötig
}
Das Paket support ist gleichzeitig sehr nützlich und sehr gefährlich. Nützlich, weil man dort kleine technische Helfer ablegen kann, die nicht zu einer bestimmten Funktion gehören. Gefährlich, weil man sehr leicht alles Mögliche dort hineinwirft und das berüchtigte util unter anderem Namen bekommt. Daher gilt die einfache Regel: support lebt nur, wenn Sie die Rolle jeder Klasse in menschlichen Worten erklären können.
Zum Beispiel macht es Sinn, den folgenden kleinen Helfer in support zu legen, weil es keine Geschäftslogik des Katalogs ist, sondern eine technische Normalisierung eines Strings:
package com.example.catalogservice.support;
public class CatalogSlugNormalizer {
public String normalize(String slug) {
// Einfachste Normalisierung: Entfernen von Leerzeichen an den Rändern und Umwandeln in Kleinbuchstaben
return slug.trim().toLowerCase();
}
}
Das Paket actuator legen wir vorab an, um später kein „Zuhause“ für Diagnosecode suchen zu müssen. Dort werden Dinge leben, die mit der Beobachtbarkeit der Anwendung zu tun haben (Health, Diagnose). Momentan kann es leer sein – das ist normal.
Und schließlich ein separater Gedanke zu static. In Spring Boot ist static ein Ordner in resources, normalerweise src/main/resources/static. Er ist für statische Dateien gedacht, die Boot als Web‑Ressourcen ausliefern kann. Das ist kein Java‑Paket. Sie sollten also nicht com.example.catalogservice.static erstellen und dort Klassen ablegen – das würde so aussehen, als ob Sie Schrank und Kühlschrank verwechselt hätten. Und beide riechen dann seltsam.
7. Paketnamen ohne Scham
Paketnamen sind kleine Entscheidungen, die sich zu einem großen Effekt summieren. Wenn ein Paket gut benannt ist, erklärt es seine Rolle. Wenn ein Paket schlecht benannt ist, kaschiert es ein Problem: Sie wissen nicht, wohin Sie Code legen sollen, und nennen das Paket daher „irgendwie“. Die häufigsten „irgendwie“ sind util, common, misc, stuff, helpers. Das sind keine Namen, sondern ein Eingeständnis von Ratlosigkeit.
Stellen Sie sich vor, man sagt Ihnen im Geschäft: „Ihre Bestellung liegt in der Abteilung ‚Sonstiges‘.“ Sie gehen in „Sonstiges“ – und dort sind Batterien, Vorhänge, Katzenfutter und ein Stick-Set. Ungefähr so sieht das Paket util aus: Zuerst scheint es praktisch, dann kann man nichts mehr darin finden, und noch schlimmer – man kann nicht verstehen, was man hinzufügen darf und was nicht.
Es ist besser, das Paket nach dem Verantwortungsbereich zu benennen. Wenn eine Klasse einen Slug normalisiert, ist das support oder sogar support.text (falls tatsächlich eine Untergruppe benötigt wird). Wenn eine Klasse zur Startkonfiguration gehört – config. Wenn eine Klasse zum Kurskatalog gehört – catalog.*.
Eine kleine Tabelle „schlechter Name → ehrlichere Alternative“ heilt normalerweise schnell den Blick:
| Schlechter Paketname | Warum schlecht | Gesündere Alternative |
|---|---|---|
| util | „alles Mögliche“, keine Grenze | support, support.text, support.logging (wenn Rolle klar) |
| common | allgemeiner Müll ohne Besitzer | konkreter Owner: catalog, config, actuator |
| misc | “miscellaneous” = „ich habe keine Lust nachzudenken“ | nach der Verantwortung der Klasse benennen |
| services im Root | mischt Funktionen in einen Haufen | catalog.service (Bindung an Funktion) |
| models im Root | unklar, Modelle wovon | catalog.domain |
Noch ein praktischer Hinweis: Machen Sie die Pakettiefe nicht um der Tiefe willen. Ein Paket com.example.catalogservice.catalog.domain.money.currency sieht so aus, als ob Sie eine Stadt bauen, in der man zum nächsten Laden mit dem Bus fahren muss. Lassen Sie die Struktur zuerst flach und verständlich sein, und Verkomplizierung tritt erst auf, wenn tatsächlich eine zweite oder dritte Klasse eines Themas auftaucht.
8. Refactoring von Paketen ohne Schmerz
Das Verschieben von Klassen zwischen Paketen klingt schlimmer, als es ist. Das Unangenehmste an diesem Prozess ist nicht die Logik, sondern die Sorgfalt: Die package ...;-Anweisung in der Datei muss mit dem tatsächlichen Pfad übereinstimmen, es dürfen keine „kaputten“ Importe übrig bleiben, und Spring muss die Komponenten weiterhin über das Component Scanning sehen. Wenn Sie ruhig vorgehen, läuft alles ziemlich mechanisch ab.
Normalerweise denke ich so: Zuerst lege ich das Root‑Paket fest, in dem CatalogServiceApplication lebt (es bleibt in com.example.catalogservice). Dann erstelle ich die Pakete der zweiten Ebene: catalog, config, support, actuator. Danach erstelle ich innerhalb von catalog die Unterpakete domain, repository, service, bootstrap, web. Und erst dann beginne ich, die Klassen nach Rollen zu verteilen – nicht „irgendwie“, sondern nach Verantwortung.
Beim Verschieben von Klassen ist es besser, das Refactoring der IDE zu verwenden (z. B. in IntelliJ IDEA ist das „Refactor → Move“), weil die IDE gleichzeitig die package-Zeile korrigiert und die Importe in den anderen Dateien aktualisiert. Wenn Sie die Verschiebung manuell über das Dateisystem durchführen, werden Sie fast garantiert 10 Minuten lang rote Unterstreichungen jagen und irgendwann anfangen, die Menschheit zu hassen. Und die IDE arbeitet im Gegensatz zur Menschheit normalerweise vorhersehbar.
Und noch ein wichtiger Punkt: Wenn Sie sehen, dass eine Klasse nicht eindeutig einzuordnen ist, ist das kein Grund, ein misc zu erstellen. Es ist ein Grund, ehrlich zu fragen: „Was ist die Rolle der Klasse?“ Manchmal lautet die Antwort: „Die Klasse ist zu verschwommen und macht alles auf einmal.“ Und das ist bereits ein Signal für ein Design‑Refactoring, nicht für die Erfindung eines neuen Müllpakets.
9. Typische Fehler beim Entwurf der Paketstruktur
Die Paketstruktur scheint ein langweiliges Thema zu sein, bis Sie ein Projekt öffnen, in dem alles „durcheinander“ liegt. Dann wird plötzlich klar, dass es nicht um Langeweile geht, sondern ums Überleben. Im Folgenden die typischen Fehler, die Anfänger am häufigsten machen und warum sie das Leben wirklich erschweren.
Fehler Nr. 1: „Ein großes Paket, später kümmere ich mich darum.“
Zuerst scheint es schneller zu sein: alles in com.example.catalogservice, und dann schauen wir weiter. Aber das „später“ kommt normalerweise nicht, und nach einer Woche erinnern Sie sich nicht mehr, welche Klasse wofür zuständig ist. Die Lösung ist einfach: Legen Sie zumindest sofort catalog und config an, und fügen Sie dann nach und nach Unterpakete hinzu, wenn Rollen entstehen.
Fehler Nr. 2: main‑Klasse in einem zu tiefen Paket.
Wenn CatalogServiceApplication in config oder catalog verschoben wird, beginnt das Component Scanning nur einen Teil des Projekts zu sehen. Das Ergebnis sind mysteriöse Fehler „Bean not found“, obwohl die Klasse scheinbar mit @Service markiert ist. Halten Sie die main‑Klasse im Wurzelpaket der Anwendung, und machen Sie alle anderen Pakete zu Unterpaketen davon.
Fehler Nr. 3: Das Paket util als „Kiste für alles“.
util hat keine Grenzen: Man kann alles hineinlegen, und daher legt man alles hinein. Niemand weiß, ob man davon abhängen darf, und was „util“ überhaupt bedeutet. Wenn Sie schon eine gemeinsame Schicht benötigen, machen Sie sie klein und ehrlich: support, und darin nur Klassen, deren Rolle Sie in einem Satz erklären können.
Fehler Nr. 4: Vermischung von Fachdomäne und Konfiguration.
Wenn Domänenmodelle neben @Configuration-Klassen liegen, hört das Projekt auf, lesbar zu sein. Der Leser versteht nicht, wo der Geschäftssinn und wo die Infrastruktur ist. Eine gute Struktur trennt diese Dinge bewusst: catalog.* – für den Katalog, config.* – für Wiring/Einstellungen.
Fehler Nr. 5: Verwechslung von Java‑Paketen und resources/static.
Anfänger erstellen manchmal ein Java‑Paket static und legen dort Klassen ab, weil „ich habe static im Projekt gesehen“. Aber static ist in Boot ein Ressourcenordner, kein Java‑Code. Vereinbaren Sie mit sich selbst: Java lebt in src/main/java, statische Dateien in src/main/resources/static, und das sind verschiedene Universen.
GO TO FULL VERSION