1. Ehrliche Vereinfachung bricht keine Grenzen
Wenn von „Lernvereinfachung" gesprochen wird, stellt man sich oft Code im Stil „na gut, für jetzt soll es irgendwie sein" vor 😅. Aber das ist die schlechte Art der Vereinfachung. Sie bricht tatsächlich die Architektur, verwischt Verantwortlichkeiten und behindert später jedes Wachstum. Wir brauchen einen anderen Ansatz: die Infrastruktur vereinfachen, nicht die Logik der Schichten selbst.
Es ist nützlich, eine einfache Tabelle im Kopf zu behalten:
| Was auf Produktionsniveau bleibt | Was bewusst vereinfacht wird |
|---|---|
| HTTP-Vertrag, Statuscodes, JSON-Antwortstruktur | statt einer echten DB machen wir die Speicherschicht in-memory |
| DTO und Grenze des externen Modells | Dateispeicher bleibt lokal |
| Validierung und Fehlervertrag | kein Auth, keine Benutzer, Rollen, Tokens |
| Struktur von Web-Layer und Service-Layer | kein Messaging, kein Cloud-Storage und kein verteiltes Runtime |
Das ist eine sehr gesunde Kombination. Wir sagen nicht: „Da wir keine Datenbank haben, können wir alles im Controller speichern." Wir sagen nicht: „Da es keine Security gibt, müssen wir Fehler nicht entwerfen." Wir sagen: „Der Hauptlernwert ist der externe API-Vertrag; alles, was nicht hilft, ihn klarer zu sehen, vereinfachen wir jetzt lieber."
Diese Entscheidung ist besonders wichtig für Kurse zum Selbststudium. Wenn ein Student den Kurs allein durcharbeitet, braucht er ein sauberes Signal. Wenn er in demselben Moment versucht, sowohl URIs als auch @ControllerAdvice als auch Lazy Loading als auch 401/403 als auch Infrastrukturdateien zu verstehen, beginnt das Lernen zu zerfallen. Ein ehrlich begrenzter Umfang wirkt hier nicht als Einschränkung, sondern als Fokus-Verstärker.
2. In-Memory-Repository
Der häufigste Fehler beim Verzicht auf eine Datenbank lautet: „Na ja, da wir vorerst keine DB haben, lassen wir einfach eine List<Task> im Controller" 🙈. Das scheint einfacher. In Wirklichkeit zerstört das Grenzen schneller, als es nutzt.
Selbst ohne DB braucht eine Anwendung trotzdem einen separaten Verantwortungspunkt für die Datenspeicherung. Deshalb bleibt im Projekt ein Repository – als Schicht und als Idee. Es ändert sich nicht die Speichergrenze selbst, sondern nur die Technologie darunter.
Ein minimales Interface könnte so aussehen:
public interface TaskRepository {
// Alle Aufgaben aus dem Speicher abrufen (Implementierungsdetails interessieren uns nicht)
List<Task> findAll();
// Aufgabe per ID suchen; Optional unterstreicht, dass die Aufgabe nicht existieren kann
Optional<Task> findById(String id);
// Aufgabe speichern (in einer DB wäre das INSERT/UPDATE, hier – Schreiben in den Speicher)
Task save(Task task);
}
Und eine einfache In-Memory-Implementierung könnte so aussehen:
public class InMemoryTaskRepository implements TaskRepository {
// Interner Speicher: Schlüssel – Aufgaben-ID, Wert – die Aufgabe selbst
// Wichtig: das ist ein Implementierungsdetail; es darf nicht nach außen „lecken"
private final Map<String, Task> storage = new HashMap<>();
@Override
public List<Task> findAll() {
// Wir geben eine Kopie der Werteliste zurück, um keinen View der Map-Kollektion nach außen zu geben
return new ArrayList<>(storage.values());
}
@Override
public Optional<Task> findById(String id) {
// Aus der Map per Schlüssel holen; wenn nicht vorhanden – null, das in Optional verpackt wird
return Optional.ofNullable(storage.get(id));
}
@Override
public Task save(Task task) {
// Aufgabe per ID speichern/überschreiben
storage.put(task.getId(), task);
// Gespeicherte Aufgabe zurückgeben (oft nützlich für die Service-Schicht)
return task;
}
}
Was ist hier wichtig zu sehen? Die Architekturform bleibt erwachsen. Es gibt eine Speicherschicht. Es gibt ein Interface. Es gibt eine Implementierung. Die Service-Schicht muss nicht wissen, dass unter der Haube eine HashMap steckt statt PostgreSQL. Der Controller erst recht nicht.
Das ist ehrliche Vereinfachung. Wir beschäftigen uns in diesem Kurs nicht mit SQL und ORM, verwandeln das Projekt aber auch nicht in Brei. Später kann die In-Memory-Implementierung durch ein JPA-Repository ersetzt werden, was bereits ein anderer Kurs mit anderem Schwerpunkt ist. Aber der Web/API-Layer sollte deswegen nicht plötzlich von Grund auf neu geschrieben werden müssen.
Hier lohnt es sich, noch etwas separat anzumerken. In-Memory-Storage macht das Projekt nicht automatisch „spielzeughaft". Ja, die Daten verschwinden nach einem Neustart. Ja, es gibt keine Transaktionen. Ja, echtes Persistence-Verhalten modellieren wir hier nicht. Aber das sind ehrlich ausgesprochene Einschränkungen, kein verstecktes Problem. Dafür siehst du sehr klar, wo die Grenze zwischen API, Service und Speicherung verläuft.
3. Warum JPA das Gespräch vorerst ablenkt
JPA ist ein mächtiges und nützliches Thema. Aber es übernimmt leicht das gesamte Gespräch 🙂. Sobald du es ins Projekt bringst, kommen mit ihm @Entity, Fetch-Strategien, Lazy Loading, ID-Generierung, Mappings, Transaktionsgrenzen, Schema-Evolution, Migrationen und eine Vielzahl von Nuancen, die eigentlich ihren eigenen Kurs verdienen.
Wenn man JPA hier „auf Vorrat" hinzufügt, findet eine einfache Aufmerksamkeitsverschiebung statt. Statt zu besprechen, welches JSON man dem Client liefert und wie man den Fehlervertrag aufbaut, diskutiert man plötzlich, warum etwas nicht gespeichert wurde, wie man Beziehungen mappt oder warum eine Liste plötzlich unnötige SQL-Anfragen stellt. Das sind alles normale Ingenieurfragen – nur eben nicht für diesen Kurs.
Es ist sehr wichtig zu verstehen, dass der Verzicht auf JPA hier nicht „Persistenz ist unwichtig" bedeutet. Es bedeutet: „Persistenz ist so wichtig, dass sie einen eigenen Fokus braucht." Dafür steht in der Kursreihe Spring Data JPA nach dem aktuellen Kurs. Zuerst stabilisierst du die äußere Grenze der Anwendung, dann lernst du, die Datenschicht daran anzubinden, ohne den Vertrag zu zerstören.
Es gibt noch einen methodischen Grund ✋. Wenn du anfängst, eine API zu früh von der Speicherstruktur her zu bauen, entsteht eine sehr gefährliche Gewohnheit: den externen Vertrag von einer Tabelle oder Entität her zu entwerfen statt vom Client-Szenario her. Dieser Kurs setzt bewusst die umgekehrte Reihenfolge. Zuerst der Client, der Vertrag und der Web-Layer. Dann die Daten und die Persistenz-Entscheidungen.
4. Dateien erfordern separate Behandlung
Mit Dateien passiert ähnliches. Wenn man die Grenzen nicht hält, gleitet das Projekt sehr schnell in eine merkwürdige Welt, in der Binärdaten anfangen, in denselben Strukturen zu leben wie normale JSON-Felder. Das ist weder für die API noch für den Speicher noch für die Architektur angenehm.
Deshalb werden im Projekt von Anfang an zwei Entitäten getrennt: Anhang-Metadaten und Dateiinhalt. Metadaten sind Teil des Domain- und API-Bilds. Für den Client sind Dateiname, Größe, Typ, Ladezeitpunkt und die Verknüpfung mit der Aufgabe wichtig. Die physische Speicherung der Bytes ist hingegen eine separate technische Linie.
Eine minimale Abstraktion für die Inhaltsspeicherung könnte so aussehen:
public interface AttachmentContentStorage {
// Dateiinhalt unter einem Schlüssel im Speicher ablegen (der Schlüssel muss nicht der Dateiname sein)
void save(String key, InputStream content) throws IOException;
// Inhalt per Schlüssel aus dem Speicher laden und in den Ziel-OutputStream schreiben
void load(String key, OutputStream target) throws IOException;
// Inhalt per Schlüssel aus dem Speicher löschen
void delete(String key) throws IOException;
}
Und eine lokale Implementierung kann mit einem gewöhnlichen Verzeichnis auf der Festplatte arbeiten:
public class LocalAttachmentContentStorage implements AttachmentContentStorage {
// Wurzelverzeichnis, in dem die Dateien physisch gespeichert werden
private final Path rootDir;
public LocalAttachmentContentStorage(Path rootDir) {
// Die Abhängigkeit (Pfad) wird von außen injiziert, um den Speicherort nicht im Code hardzucoden
this.rootDir = rootDir;
}
@Override
public void save(String key, InputStream content) throws IOException {
// Verzeichnis bei Bedarf erstellen (wenn es bereits existiert – kein Fehler)
Files.createDirectories(rootDir);
// Bytes aus dem Eingabestream in eine Datei auf der Festplatte kopieren, bei erneutem Upload ersetzen
Files.copy(content, rootDir.resolve(key), StandardCopyOption.REPLACE_EXISTING);
}
// load/delete aus Kürze weggelassen
}
Warum ist das schon jetzt nützlich? Weil du im Voraus verhinderst, dass das Datei-Thema das Projekt auseinanderfallen lässt. Der Controller darf nicht selbst direkt ins Dateisystem gehen. Der Service darf kein byte[] innerhalb der Aufgabe speichern. Metadaten dürfen nicht mit dem physischen Speicherpfad vermischt werden. Selbst wenn die Infrastruktur vereinfacht ist, bleiben die Grenzen erwachsen.
5. Ohne Security sieht man die API selbst besser 🔐
Bei Security ist die Situation ähnlich wie bei JPA, nur noch sensibler. Sobald du Authentication und Authorization hinzufügst, ändert sich nicht nur die interne Struktur des Projekts, sondern auch die externe Semantik der Anfragen selbst. Es kommen 401, 403, Ownership-Regeln, Rollen, User-Kontext, Password-Handling, Filter, Tokens und eine lange Kette von Zugangsentscheidungen hinzu.
Das ist ein sehr wichtiges Thema. Aber in einem Kurs über REST API-Design wird es leicht zu laut 🔊. Statt in Ruhe das Ressourcenmodell, DTOs, Validierung und Error Handling zu besprechen, muss man bei jedem Schritt überlegen: Wer ist gerade der Benutzer, wie wird seine Identität weitergegeben, warum wird dieses Szenario als Forbidden behandelt, wie schreibt man Tests mit Auth, wo liegt die Grenze zwischen Geschäftsregel und Sicherheitsregel.
Für den ersten großen Kurs über die Web/API-Schicht ist das ein zu starkes Ablenkungsmoment 🚧. Deshalb bleibt das Projekt bewusst ohne Security. Nicht weil Security nicht gebraucht wird. Sondern weil es jetzt nützlich ist, zuerst den „sauberen" Vertrag zu sehen: Ressourcen, Operationen, Datenstrukturen, Fehler und die Regeln des normalen API-Verhaltens.
Das ist auch dem Nachbarkurs gegenüber ehrlich. Spring Security existiert nicht als „Anhängsel", sondern als eigene Disziplin. Wenn du dort bereits mit einem ordentlichen Ressourcen- und Fehlermodell ankommst, legt sich Security auf eine verständliche Grundlage, nicht auf ein chaotisches Bündel von Endpoints.
6. Wie diese Struktur später in Data und Security weitergeführt wird 🌉
Eine der stärksten Seiten der aktuellen Entscheidung ist, dass sie kein Sackgasse ist. Das ist keine „Spielzeugarchitektur, die man später wegwerfen muss". Im Gegenteil: Das ist eine Projektform, die problemlos weiterlebt.
Schau dir diese einfache Übersicht an:
| Jetzt im Kurs | Was später hinzukommen kann, ohne den Web-Layer zu zerstören |
|---|---|
| InMemoryTaskRepository | JpaTaskRepository oder ein anderer Persistence-Adapter |
| LocalAttachmentContentStorage | S3-like storage oder ein anderer Datei-Adapter |
| kein Auth | Security Filter Chain, User-Modell, Business-Autorisierung |
| DTO und Fehlervertrag | bleiben dieselbe äußere Grenze |
| Controller → Service → Repository | bleibt erhalten, interne Implementierungen ändern sich |
Das ist ein sehr wichtiger Punkt. Unser Kurs tut nicht so, als wäre In-Memory besser als eine echte Datenbank oder ein lokaler Ordner besser als ein richtiger Speicher. Der Kurs zeigt etwas anderes: Externe Vertrag und Architekturgrenzen können bereits jetzt aufgebaut werden, ohne auf alle schweren Technologien zu warten.
Später, wenn die Datenschicht hinzukommt, sollte ein richtig gemachter Web/API-Layer nicht aus den Nähten platzen. Dann, wenn Security hinzukommt, sollten gut entworfene Ressourcen, Statuscodes und das Fehlermodell nicht plötzlich zufällig werden. Genau deshalb ist die Reihenfolge der Themen in der Kursreihe so, wie sie ist: Zuerst wird die API-Grenze stabilisiert, dann werden die schwereren Schichten daran angebunden.
7. Typische Fehler bei der Vereinfachung der Infrastruktur 🚧
Fehler Nr. 1: „Da es keine DB gibt, brauchen wir kein Repository".
Das ist einer der schnellsten Wege, das Projekt in Chaos zu verwandeln. Die Speicherung muss trotzdem hinter einer eigenen Grenze leben. Sonst beginnt sie sich über Services und Controller zu verteilen.
Fehler Nr. 2: Daten direkt im Controller halten, weil es schneller geht.
Schneller – ja. Besser – fast nie. Der Controller soll sich mit der Web/API-Aufgabe beschäftigen und nicht gleichzeitig Transport, Service und Speicher sein.
Fehler Nr. 3: Versuchen, „ein bisschen JPA" nur für eine Entität hinzuzufügen.
Ein solches Hybridprojekt ist methodisch fast immer schlechter als ein ehrlich vereinfachtes. Es entstehen zwei Welten auf einmal: Ein Teil des Verhaltens wird von der Datenschicht diktiert, ein anderer nicht, und für den Lernenden wird es schwieriger zu unterscheiden, was er gerade genau lernt.
Fehler Nr. 4: Dateiinhalt direkt im Domain-Modell der Aufgabe speichern.
Das wirkt nur im Moment bequem. Danach kommen schwere Antworten, Speicherprobleme und das Vermischen von Metadaten und Inhalt. Die separate Storage-Abstraktion ist hier nicht der Schönheit wegen, sondern für die Gesundheit des Projekts.
Fehler Nr. 5: Das Fehlen von Security als „Unseriösität" wahrnehmen.
Tatsächlich ist das einfach eine bewusste Kursgrenze. Hier lernt man zunächst, eine saubere und vorhersehbare API-Grenze aufzubauen. Security kommt auf dem nächsten Level, wenn diese Grenze bereits klar ist.
GO TO FULL VERSION