WInf-Projekt · Technik

System­architektur.

Schritt für Schritt vom Browser bis zum Server, an einem durchgehenden Beispiel.

Weiter mit → oder Leertaste · Übersicht der Kapitel unten in der Leiste
Das Beispiel für heute

Ein Urlaubsantrag.

Eine Mitarbeiterin beantragt zwei Wochen Urlaub. Ihre Teamleitung prüft den Antrag und genehmigt ihn. Danach sinkt der Resturlaub um zehn Tage.

Klingt nach einem Formular und einer Tabelle. Wir bauen trotzdem die ganze Architektur des Projekts darum herum, denn jede Zeile der Geschichte stellt eine Anforderung.

Von der Geschichte zur Architektur

Jede Anforderung bringt einen Baustein.

AnforderungBausteinSchritt
Mitarbeitende bedienen das System im BrowserReact SPA1
Anträge und Resturlaub werden dauerhaft gespeichertMicroservices PostgreSQL2
Der Antrag durchläuft einen festen Ablauf mit GenehmigungCIB seven3
Nur die Teamleitung darf genehmigenKeycloak4
Alles ist sicher unter einer Adresse erreichbarNginx Proxy Manager5
Neue Versionen kommen kontrolliert auf den ServerGitHub Actions, Portainer6
Schritt 1 · Oberfläche

Womit arbeitet die Mitarbeiterin?

→Mit einer Webanwendung, die komplett im Browser läuft.

1
Schritt 1 · React SPA

Eine App, die im Browser lebt.

Eine Single Page Application wird einmal geladen: HTML, JavaScript, CSS. Danach läuft sie auf dem Rechner der Nutzerin und holt nur noch Daten nach, als JSON über REST.

src/AntragsListe.tsxconst api = import.meta.env.VITE_API_URL;
const res = await fetch(`${api}/antraege`);
const antraege = await res.json();
// [{ id: 42, von: "2026-08-03", bis: "2026-08-14" }]
Typischer Denkfehler

„Das Frontend läuft auf dem Server.“ Der Server liefert die Dateien nur aus. Jeder Aufruf, den die App macht, kommt aus dem Browser, also von außen.

Schritt 2 · Fachdaten

Wo liegt der Antrag, wenn der Browser zu ist?

→In einem Microservice mit eigener Datenbank.

2
Schritt 2 · Urlaubs-Service

Ein Service pro Fachthema.

Der Urlaubs-Service kennt genau ein Thema: Anträge. Panache speichert sie in PostgreSQL, REST Jackson liefert sie als JSON. Kein SQL, kein Mapping-Code.

Urlaubsantrag.java · UrlaubsantragResource.java@Entity
public class Urlaubsantrag extends PanacheEntity {
    public String mitarbeiterId;
    public LocalDate von, bis;
}
@Path("/antraege")
public class UrlaubsantragResource {
    @GET
    public List<Urlaubsantrag> alle() {
        return Urlaubsantrag.listAll();
    }
}
Schritt 2 · Mitarbeiter-Service

Der Resturlaub gehört woanders hin.

Resturlaub, Team und Vorgesetzte sind ein anderes Fachthema. Also bekommt es einen eigenen Mitarbeiter-Service mit eigener Datenbank.

Verlockend

Eine Datenbank für alle

Der Urlaubs-Service liest die Tabelle mitarbeiter direkt. Benennt jemand eine Spalte um, bricht ein fremder Service.

Unser Weg

Jeder Service besitzt seine Daten

Wer Resturlaub braucht, fragt die REST-API des Mitarbeiter-Service. Jeder Service wird eigenständig versioniert und deployed.

Schritt 3 · Prozess

Wer weiß, wo Antrag 42 gerade steht?

→Eine Process Engine, die den Ablauf als BPMN ausführt.

3
Schritt 3 · Das Problem

Abläufe wuchern im Code.

Ohne Process Engineif (antrag.status == OFFEN && user.istTeamleitung()) {
    antrag.status = genehmigt ? GENEHMIGT : ABGELEHNT;
    if (genehmigt) {
        mitarbeiterClient.bucheResturlaub(antrag);
        // und wenn der Mitarbeiter-Service gerade down ist?
    }
}
// Erinnerung nach 3 Tagen? Vertretung im Urlaub?
// Wer sieht, welche Anträge bei wem liegen?
  • Der Ablauf steht nirgends am Stück, nur verteilt über Statusfelder und ifs.
  • Wartezeiten, Fristen und Fehlerfälle werden zu Eigenbau.
  • Die Fachabteilung kann den Ablauf weder lesen noch prüfen.
Idee

Den Ablauf modellieren statt programmieren und von einer Engine ausführen lassen.

Schritt 3 · Der Ablauf als BPMN

Das Modell ist das Programm.

ja nein Antrag gestellt Antrag prüfenUser Task · Teamleitung genehmigt? Resturlaub buchenService Task · ruft Mitarbeiter-Service genehmigt abgelehnt
antragId = 42 genehmigt = true

Der Antrag startet eine Prozessinstanz. Sie trägt nur die Referenz antragId = 42, die Antragsdaten bleiben im Urlaubs-Service.

Die Engine wartet an der User Task, Stunden oder Wochen, und weiß jederzeit, bei wem der Antrag liegt.

Nach der Genehmigung ruft die Engine selbst den Mitarbeiter-Service auf. Sie orchestriert die Services.

Schritt 3 · CIB seven

Die Engine in der Mitte.

CIB seven ist ein Fork von Camunda 7. Eine zentrale Instanz führt die BPMN-Prozesse aller Teams aus. Auf dem Server gibt es keine Weboberfläche, nur die REST API unter /engine-rest.

CIB seven weiß

Wie weit der Ablauf ist

Welche Instanzen laufen, welche Task offen ist, welche Variablen gesetzt sind.

Die Services wissen

Was ein Antrag ist

Zeitraum, Person, Resturlaub. Die fachliche Wahrheit bleibt in ihren Datenbanken.

Im Prozess stehen deshalb meist nur IDs. Wie man Fachdaten bei Bedarf nachlädt, zeigt die Seite Daten in BPMN Prozessen.

Schritt 3 · Wen fragt die SPA?

Fachdaten oder Prozess?

Fachdaten → Microservice

GET https://urlaub.winfprojekt.de/antraege/42
→ { "id": 42, "von": "2026-08-03", "bis": "2026-08-14" }

Prozess → CIB seven

GET https://cibseven.winfprojekt.de/engine-rest
    /task?assignee=teamleitung
→ [{ "name": "Antrag prüfen", "processInstanceId": "…" }]

Eine Liste von Anträgen anzeigen: Microservice. „Welche Tasks sind bei mir offen?“ oder Prozess starten: CIB seven.

Schritt 4 · Identität

Woher weiß der Service, wer anfragt?

→Aus einem signierten Token, ausgestellt von Keycloak.

4
Schritt 4 · Keycloak

Ein Login für alle.

Drei Services mit drei Passwort-Tabellen wären drei Angriffsflächen. Stattdessen übernimmt Keycloak als zentraler Identity Provider Login, Rollen und Token-Ausgabe nach OpenID Connect.

  1. SPA → KeycloakDie App leitet zum Login-Formular von Keycloak weiter. Das Passwort sieht nur Keycloak.
  2. Keycloak → SPANach dem Login bekommt die App ein Access Token (JWT), per Authorization Code Flow mit PKCE.
  3. SPA → ServiceJeder Aufruf trägt den Header Authorization: Bearer ….
  4. Service ↔ KeycloakServices und CIB seven prüfen die Signatur mit dem öffentlichen Schlüssel von Keycloak (JWKS). Der wird einmal geholt und gecacht.
Schritt 4 · Ein Blick ins Token

Ein JWT ist ein signierter Ausweis.

eyJhbGciOiJSUzI1NiIsImtpZCI6InJlYWxtLWtleSJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZWFtbGVpdHVuZyIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJ0ZWFtbGVpdHVuZyJdfSwiZXhwIjoxNzkxODA2NDAwfQ.Vb3x9kQm2pT7rZ0aLw5sYc8nH1fJ4dE6uK2oP9qR3tW
Header

Wie signiert?

{ "alg": "RS256",
  "kid": "realm-key" }
Payload

Wer, was, wie lange?

{ "preferred_username": "teamleitung",
  "realm_access": { "roles": ["teamleitung"] },
  "exp": 1791806400 }
Signatur

Echt?

Nur Keycloak hat den privaten Schlüssel. Jeder Service prüft mit dem öffentlichen, ohne Keycloak zu fragen.

Im Service reicht dann eine Annotation: @RolesAllowed("teamleitung") auf dem Endpunkt zum Genehmigen. Die Extension quarkus-oidc erledigt den Rest.

Schritt 5 · Eingang

Sechs Container, aber nur eine Tür?

→Ein Reverse Proxy nimmt alles an und verteilt nach Subdomain.

5
Schritt 5 · Nginx Proxy Manager

Eine Tür, viele Räume.

Nginx Proxy Manager ist der einzige Dienst, der aus dem Internet erreichbar ist. Er terminiert TLS mit Let's-Encrypt-Zertifikaten und leitet jede Subdomain an ihren Container weiter.

Öffentlich (HTTPS)Intern (HTTP)
frontend.winfprojekt.defrontend:80
urlaub.winfprojekt.deurlaub-service:8080
cibseven.winfprojekt.decibseven:8080
keycloak.winfprojekt.dekeycloak:8080

Deshalb zeigt der Bauplan nur eine Kante zur SPA: jeder Aufruf aus dem Browser, auch an Services, CIB seven und Keycloak, geht durch diese Tür.

Schritt 5 · Wie der Proxy die Container findet

Gemeinsames Netz statt offener Ports.

docker-compose.yml · urlaub-serviceservices:
  urlaub-service:
    image: ghcr.io/org/urlaub-service:1.2.0
    networks: [nginx-reverse-proxy, intern]
  datenbank:
    image: postgres:16
    networks: [intern]   # nur intern

networks:
  nginx-reverse-proxy: { external: true }
  intern: { driver: bridge }
  • nginx-reverse-proxy ist ein externes Netz auf dem Host. Wer darin hängt, ist für den Proxy per Servicename erreichbar.
  • Die Datenbank hängt nur im Netz intern. Sie ist für den Service erreichbar, aber nie von außen.
  • Kein ports:-Eintrag. Ein offener Host-Port würde am Proxy vorbei führen.
Schritt 6 · Betrieb

Wie kommt Version 1.2.0 auf den Server?

→Als Docker-Image, gebaut von der Pipeline, ausgerollt mit Portainer.

6
Schritt 6 · Vom Commit zum Container

Gebaut wird automatisch, ausgerollt bewusst.

tag

Git-Tag v1.2.0

Push auf main oder ein Versions-Tag nach Semantic Versioning.

CI

GitHub Action

Maven-Build, Docker-Image bauen, alles im Workflow docker.yml.

img

GHCR

Das Image liegt als ghcr.io/…:1.2.0 in der Registry.

run

Portainer

Tag in der docker-compose.yml hochziehen, dann Pull and redeploy.

Ein Artefakt, zwei Welten

Konfiguration aus der Umgebung

In Produktion setzt Portainer QUARKUS_DATASOURCE_JDBC_URL und QUARKUS_OIDC_AUTH_SERVER_URL. Im Repository stehen keine URLs und keine Passwörter.

Lokal

Dev Services

Ist nichts konfiguriert, startet Quarkus PostgreSQL und Keycloak selbst als Container. Deshalb bleibt die application.properties leer.

Das Gesamtbild

Alles zusammen.

Jeder Baustein beantwortet genau eine Frage aus der Geschichte.

Fahrt mit der Maus über die Bausteine im Plan.

Selbst entwickelt

React SPA, die Microservices und die BPMN-Prozesse. Proxy, Keycloak, Datenbanken und die Engine selbst werden nur betrieben.

Ein Antrag, einmal durch alles

Der Durchlauf.

  1. Browser → Proxy → SPAApp laden über frontend.winfprojekt.de
  2. SPA → KeycloakEinloggen, Token erhalten
  3. SPA → Urlaubs-ServiceAntrag anlegen, er bekommt die ID 42
  4. SPA → CIB sevenProzess starten mit antragId = 42
  5. Teamleitung · SPA → CIB sevenOffene Tasks: „Antrag prüfen“
  6. SPA → Urlaubs-ServiceAntrag 42 laden und anzeigen
  7. SPA → CIB sevenTask abschließen, genehmigt = true
  8. CIB seven → Mitarbeiter-ServiceService Task bucht zehn Tage Resturlaub ab
Zum Mitnehmen

Sechs Faustregeln.

Die SPA läuft im Browser

Jeder ihrer Aufrufe kommt von außen und braucht ein Token.

Ein Service, ein Fachthema

Mit eigener Datenbank. Fremde Daten nur über deren REST-API.

Abläufe gehören in BPMN

CIB seven hält den Zustand, im Prozess stehen IDs statt Kopien.

Fachdaten oder Prozess?

Liste anzeigen: Microservice. Offene Tasks, Prozess starten: CIB seven.

Keycloak stellt aus, alle prüfen

Services validieren das JWT selbst mit dem öffentlichen Schlüssel.

Eine Tür nach außen

Nur der Proxy ist öffentlich. Keine ports:, Konfiguration aus Umgebungsvariablen.

Bauplan Server · Docker-Container · Portainer Fachdaten Fachdaten Prozess · Tasks JDBC JDBC JWKS Login BrowserMitarbeiterin · Teamleitung Nginx Proxy ManagerTLS · Subdomains neu React SPAVite · keycloak-js neu Urlaubs-ServiceQuarkus · Anträge neu CIB sevenBPMN · Orchestrator neu Mitarbeiter-ServiceQuarkus · Resturlaub neu PostgreSQLAnträge PostgreSQLMitarbeitende KeycloakOIDC · stellt JWTs aus neu