Legacy Claims & Customer Support Enterprise System
Vollständige interaktive Lesefassung mit dem unveränderten Gesamtinhalt, zusätzlicher Schnellnavigation und fachlich kompakten Architekturvisualisierungen.
Konsolidierte, planungsbereinigte Fassung aus dem Rohmaterial. Reine Planungsabschnitte wurden entfernt.
Schnellinhaltsverzeichnis
Alle Hauptkapitel sind vollständig enthalten. Ein Klick öffnet das Kapitel und springt direkt zum Inhalt.
011. Einführung und Lesehinweis
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Einführung und Lesehinweis in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Projektüberblick
Konsolidierte, ausführliche Lernbuchfassung aus dem vorhandenen Rohmaterial. Diese Fassung strukturiert das Rohmaterial einheitlich, entfernt nur offensichtliche Chat-/Runden-Artefakte und bewahrt die fachlichen Inhalte, Codebeispiele, Tabellen, Mapping- und Migrationsabschnitte.
Lesehinweis
Dieses Handbuch ist als durchgehendes Lern- und Nachschlagewerk aufgebaut. Die HTML-Versionen enthalten klappbare Hauptkapitel, Navigation, Suche, echte Markdown-Tabellen, kompakte JetBrains-Dark-Codeblöcke und dezente SVG-Visuals.
Konsolidierte finale Fassung aus den Rohmaterial-Runden 01-07 und den Änderungsdokumenten. Der Inhalt wurde nicht gekürzt. Tabellen und Codeblöcke wurden für HTML/PDF strukturell sauber aufbereitet.
Konsolidierte Struktur
- Teil 1: Zielbild und Systemlandschaft
- Teil 2: Legacy-Ausgangssystem
- Teil 3: Refactoring Schritt fuer Schritt
- Teil 4: Moderne Maven-Zielstruktur
- Teil 5: Migration nach OpenShift
- Teil 6: Lernbuch-Artefakte
- Teil 7: Refactoring-Phasen und Change-Dokumentation
- Anhaenge: Changelog, Mapping, Risiken und Codebeispiele
022. Zielbild und Gesamtstruktur
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Zielbild und Gesamtstruktur in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Zielbild, Systemlandschaft und Gesamtstruktur
Rundennummer: 01 Datum: 2026-07-05 Arbeitsmodus: Große Runde 1 Projektname: Legacy Claims & Customer Support Enterprise System Ziel dieser Runde: Zielbild, Systemlandschaft, Gesamtstruktur, Modulgrenzen, Artefaktstruktur und Migrations-/Refactoring-Rahmen definieren.
Ergebnis dieser Runde
In dieser Runde wurde das Projekt als großes Enterprise-Lernprojekt
strukturiert. Der Fokus liegt noch nicht auf vollständigem Legacy-Code
und noch nicht auf der Monster Method
processClaimDecision(...). Diese Themen kommen in Runde 2
und Runde 3.
Runde 1 liefert die fachliche und technische Landkarte:
- Zielbild der Gesamtarchitektur
- Legacy-Systemlandschaft
- Zusatzsysteme als echte Architekturbausteine
- Zielmodule für ein modernes, modulares Maven-Projekt
- Refactoring- und Migrationsphasen
- Artefaktstruktur für Rohmaterial, Lernbuch, Beispielprojekt, Changelog, Before/After-Mapping und finale Pakete
- erste Package- und Modulregeln
- erste Code-Skelette für Architekturgrenzen
- Vorbereitung für spätere SVG-Grafiken und iPhone-/Safari-taugliche HTML-Versionen
Zielbild des Lernprojekts
Das Projekt soll zeigen, wie man eine alte Java-Enterprise-Landschaft nicht einfach „neu schreibt“, sondern methodisch versteht, absichert, schrittweise refactored und kontrolliert nach OpenShift migriert.
Das Lernprojekt besteht aus vier großen Sichtweisen:
Legacy verstehen WebSphere Traditional, EAR/WAR/EJB, SOAP, JMS, JTA, Oracle, JSP/Servlet, JAAS/LDAP, File Shares, Scheduler, Stored Procedures und manuelle Deployments.
Legacy absichern Characterization Tests, Golden Master Tests, SOAP Contract Tests, Adapter Tests, Migration Tests, Architekturtests und Testdaten-Builder.
Legacy schrittweise refactoren Fachlogik aus EJB, SOAP, JDBC, JMS und File-System-Abhängigkeiten herauslösen. Die Monster Method
processClaimDecision(...)wird später in mehrere fachliche Schritte zerlegt.Modernisieren und migrieren Modulare Maven-Struktur, Ports & Adapters, Open Liberty/Spring Boot/Jakarta-Hybrid, OpenAPI/REST, Outbox, Idempotency, Observability, Security-Migration und OpenShift Deployment.
Leitprinzipien
Keine Big-Bang-Migration
Das Lernprojekt soll bewusst zeigen, warum eine Big-Bang-Migration gefährlich ist. Ein altes Claims-System enthält fachliche Regeln, technische Sonderfälle, historische Datenannahmen und Integrationslogik. Diese Dinge sind oft nicht vollständig dokumentiert. Deshalb braucht das Projekt einen schrittweisen Ansatz.
Merksatz: Eine erfolgreiche Migration beginnt nicht mit Kubernetes-YAML, sondern mit Verständnis, Tests und klaren Grenzen.
Fachlogik aus technischen Schichten befreien
Im Legacy-System steckt Fachlogik häufig direkt in:
- EJB Session Beans
- SOAP Endpoint-Klassen
- JSP/Servlet-Controllern
- DAO-Klassen
- Stored-Procedure-Aufrufen
- JMS-Publishern
- TimerBeans
- File-Share-Clients
- JAAS-Rollenprüfungen
Das Ziel ist nicht, alle Technik sofort zu ersetzen. Das Ziel ist zuerst, die Fachlogik sichtbar und testbar zu machen.
Zusatzsysteme als echte Teile der Architektur behandeln
Die Zusatzsysteme werden nicht als Randnotiz behandelt. Sie sind wichtige Quellen technischer Kopplung:
- Partner Portal
- Internal Backoffice
- Document Management Adapter
- Fraud Risk Gateway
- Policy Coverage System
- Payment Release System
- Notification System
- SLA & Escalation Scheduler
- Audit & Compliance Reporter
- Search Indexer
- Customer Master Data System
- Identity & Role Management
Jedes Zusatzsystem bekommt später eigene Codebeispiele, Adapter, Tests, Before/After-Mapping und Einträge im Changelog.
Rohmaterial bleibt erhalten
Nach jeder Runde entsteht eine neue Roh-MD-Datei. Diese Rohdateien werden nicht überschrieben. Später entsteht zusätzlich eine konsolidierte schöne Fassung.
Gesamt-Systemlandschaft
Systemlandschaft als Verantwortungsfluss
Die fachliche Entscheidung bleibt im Claims Core; Kanäle und Partnersysteme werden über explizite Adapter angebunden.
- Kanäle liefern Anfragen, besitzen aber keine Claims-Regeln.
- Adapter übersetzen Protokolle und Datenmodelle, ohne Fachlogik zu übernehmen.
- Externe Systeme werden als eigene Ausfall- und Konsistenzgrenzen behandelt.
Fachlicher Mittelpunkt
Im Zentrum steht der Schadensfall:
Claim
├─ Kunde
├─ Police / Vertrag / Deckung
├─ Dokumente
├─ Fraud-Risiko
├─ Status / Workflow
├─ Entscheidung
├─ Zahlung
├─ Audit Trail
├─ SLA / Eskalation
└─ Kommunikation / BenachrichtigungDer Schadensfall ist nicht nur ein Datensatz. Er ist ein fachlicher Prozess. Der Prozess umfasst Erfassung, Dokumentenprüfung, Deckungsprüfung, Betrugsrisikobewertung, Entscheidung, Genehmigung, Zahlung, Benachrichtigung und Auditierbarkeit.
Legacy-Landschaft als Textdiagramm
+----------------------------------+
| Legacy Identity & Role Management |
| LDAP / JAAS / WebSphere Roles |
+-------------------+--------------+
|
v
+-----------------------------+ SOAP +-----------------------------+
| Legacy Claims Partner Portal| -----------> | Legacy Claims Facade EJB |
| JSP / Servlet / Upload | | WebSphere / EAR / EJB-JAR |
+-----------------------------+ +-------------+---------------+
|
+-----------------------------+ |
| Legacy Internal Backoffice | JSP/EJB/Stored Procedures |
| JSP / Servlet / Oracle | ------------------------------+
+-----------------------------+ |
v
+--------------------------------------+
| Monster Method |
| processClaimDecision(...) |
| if/else, SOAP, JMS, JDBC, Files, JAAS |
+---+-----------+-----------+----------+
| | |
+---------------------+ | +----------------------+
v v v
+-----------------------------+ +------------------------------+ +-----------------------------+
| Policy Coverage System | | Fraud Risk Gateway | | Document Management Adapter |
| SOAP / Oracle / Rules | | SOAP / XML / Timeout Issues | | File Share / SOAP / XML |
+-----------------------------+ +------------------------------+ +-----------------------------+
|
v
+-----------------------------+ +------------------------------+ +-----------------------------+
| Payment Release System | | Notification System | | Audit & Compliance Reporter |
| SOAP / JMS / JTA | | JMS / SOAP / Email / SMS | | Stored Procedures / CSV |
+-----------------------------+ +------------------------------+ +-----------------------------+
+-----------------------------+ +------------------------------+ +-----------------------------+
| SLA & Escalation Scheduler | | Legacy Search Indexer | | Customer Master Data System |
| EJB Timer / Scheduler | | Oracle LIKE / Batch Indexer | | SOAP / Oracle / Customer ID |
+-----------------------------+ +------------------------------+ +-----------------------------+Ziel-Landschaft als Textdiagramm
+----------------------------------------------------------------------------------+
| OpenShift |
| +------------------+ +----------------------+ +------------------+ |
| | claim-rest-api | ----> | claim-application | ----> | claim-domain | |
| | REST / OpenAPI | | Use Cases / Commands | | Entities/Policy | |
| +------------------+ +----------+-----------+ +------------------+ |
| | |
| +-----------------------+------------------------+ |
| | | | |
| v v v |
| +---------------------+ +----------------------+ +-------------------------+ |
| | claim-document | | claim-workflow | | claim-payment | |
| | Ports + Adapters | | Orchestrator/State | | Port/Adapter/Outbox | |
| +---------------------+ +----------------------+ +-------------------------+ |
| | | | |
| v v v |
| +---------------------+ +----------------------+ +-------------------------+ |
| | claim-audit | | claim-notification | | claim-search | |
| | Audit Trail | | Events/Templates | | Read Model/Search Port | |
| +---------------------+ +----------------------+ +-------------------------+ |
| +------------------+ +---------------------+ +-------------------------------+ |
| | claim-batch | | claim-soap-api | | claim-infrastructure | |
| | CronJob Worker | | SOAP Compatibility | | SOAP/JMS/DB/File/Observability| |
| +------------------+ +---------------------+ +-------------------------------+ |
+----------------------------------------------------------------------------------+Domänenlandkarte
Core Domain
Die zentrale Fachdomäne ist das Claims Management:
- Schaden erfassen
- Schaden validieren
- Deckung prüfen
- Dokumente prüfen
- Fraud-Risiko prüfen
- Entscheidung treffen
- Zahlung vorbereiten
- Zahlung genehmigen
- Ablehnung erklären
- Audit Trail erzeugen
- SLA überwachen
Supporting Domains
Supporting Domains unterstützen die Kernentscheidung:
| Supporting Domain | Aufgabe | Typische Legacy-Kopplung | Moderne Grenze |
|---|---|---|---|
| Document Management | Dokumente speichern, prüfen, archivieren | File Share, SOAP, XML-Metadaten | DocumentStoragePort,
DocumentVerificationPort |
| Fraud Risk | Risiko prüfen, Score abrufen | SOAP-Client direkt in EJB | FraudRiskPort |
| Policy Coverage | Deckung und Ausschlüsse prüfen | SOAP + Stored Procedures | CoveragePort, CoveragePolicy |
| Payment Release | Zahlung vorbereiten/freigeben | SOAP/JMS/JTA direkt im Workflow | PaymentReleasePort, Outbox |
| Notification | Kunden/Partner/Agenten informieren | JMS/SOAP/Template-Dateien | NotificationPort |
| Audit/Compliance | Entscheidungen nachvollziehbar machen | Stored Procedures, technische Logs | AuditLogPort, Audit Events |
| Search | Claims/Kunden/Dokumente suchen | Oracle LIKE, Batch Indexer | ClaimSearchPort |
| SLA/Escalation | Fristen, Verletzungen, Eskalationen | TimerBean, Scheduler, Stored Proc | EscalationPolicy, claim-batch |
| Customer Master Data | Kundendaten abrufen | SOAP, alte Kundennummern | CustomerMasterDataPort |
| Identity/Role | Benutzer/Rollen prüfen | LDAP/JAAS/WebSphere Roles | SecurityContextPort,
AuthorizationPolicy |
Integrationsdomänen
Integrationen sind im Zielsystem keine zufälligen technischen Aufrufe mehr, sondern explizite Grenzen:
Application Use Case
-> Port Interface
-> Adapter
-> Legacy SOAP / JMS / DB / File Share / OpenShift ServiceZielmodule
Die Zielmodule bilden fachliche und technische Grenzen. Der Package-Stamm lautet immer:
com.seb4u.demo.claimsModulübersicht
| Modul | Zweck | Enthält |
|---|---|---|
shared-kernel |
Gemeinsame Basistypen | IDs, Money, Result, Error Codes, Clock, CorrelationId |
claim-domain |
Reine Fachlogik | Claim, ClaimStatus, Policies, State, Value Objects |
claim-application |
Use Cases | Command Handler, Application Services, Ports |
claim-infrastructure |
technische Adapter | SOAP Clients, JMS, JDBC, JPA, File Share, Observability |
claim-soap-api |
Legacy-kompatible SOAP Boundary | WSDL-kompatible Endpoints, DTO Mapping |
claim-rest-api |
moderne API | REST Controller, DTOs, OpenAPI-Anbindung |
claim-batch |
Batch/Scheduler | Escalation Jobs, SLA Worker, OpenShift CronJob |
claim-document |
Dokumentengrenze | Document Ports, Document Policies, Adapters |
claim-workflow |
Workflow | Orchestrator, State Machine, Approval Chain |
claim-search |
Suche | Search Port, Search Read Model, Adapter |
claim-notification |
Benachrichtigung | Notification Port, Templates, Events |
claim-audit |
Audit Trail | Audit Events, AuditLogPort, Reporting Read Model |
claim-payment |
Zahlung | PaymentReleasePort, Idempotency, Compensation |
claim-openapi |
API-Spezifikation | OpenAPI YAML/JSON, generated docs |
migration-tests |
Migrationssicherung | SOAP/REST Compatibility, Contract, Smoke Tests |
legacy-websphere-ear |
Legacy Ausgangspunkt | EAR/WAR/EJB Struktur |
legacy-claims-partner-portal |
externes Portal | JSP/Servlet/SOAP Client |
legacy-internal-claims-backoffice |
internes Backoffice | JSP/EJB/Stored Proc |
legacy-document-management-adapter |
DMS Legacy | File Share/SOAP/XML |
legacy-fraud-risk-gateway |
Fraud Legacy | SOAP/XML |
legacy-policy-coverage-system |
Coverage Legacy | SOAP/Oracle/Stored Proc |
legacy-payment-release-system |
Payment Legacy | SOAP/JMS/JTA |
legacy-notification-system |
Notification Legacy | JMS/SOAP/Email/SMS |
legacy-sla-escalation-scheduler |
SLA Legacy | TimerBean/Scheduler |
legacy-audit-compliance-reporter |
Reporting Legacy | Stored Procedures/CSV |
legacy-search-indexer |
Search Legacy | Oracle LIKE/Batch |
legacy-customer-master-data-system |
Customer Legacy | SOAP/Oracle |
legacy-identity-role-management |
Identity Legacy | LDAP/JAAS/Roles |
Vorgeschlagene Gesamtstruktur des Beispielprojekts
legacy-claims-customer-support-enterprise/
README.md
pom.xml
before_refactoring_legacy/
legacy-websphere-ear/
legacy-claims-partner-portal/
legacy-internal-claims-backoffice/
legacy-document-management-adapter/
legacy-fraud-risk-gateway/
legacy-policy-coverage-system/
legacy-payment-release-system/
legacy-notification-system/
legacy-sla-escalation-scheduler/
legacy-audit-compliance-reporter/
legacy-search-indexer/
legacy-customer-master-data-system/
legacy-identity-role-management/
refactoring_steps/
step_01_inventory_characterization/
step_02_system_landscape/
step_03_monster_method_understanding/
step_04_extract_facade_and_command/
step_05_extract_state_policy/
step_06_extract_coverage_fraud_document_payment_ports/
step_07_extract_customer_identity_ports/
step_08_workflow_orchestrator/
step_09_audit_trail_abstraction/
step_10_search_notification_reporting_split/
step_11_outbox_idempotency/
step_12_batch_sla_escalation/
step_13_portal_backoffice_decoupling/
step_14_openshift_preparation/
after_refactoring_modular/
shared-kernel/
claim-domain/
claim-application/
claim-infrastructure/
claim-soap-api/
claim-rest-api/
claim-batch/
claim-document/
claim-workflow/
claim-search/
claim-notification/
claim-audit/
claim-payment/
claim-openapi/
migration-tests/
migration_to_openshift/
docker/
openshift/
base/
overlays/
dev/
test/
prod/
config/
secrets-examples/
runbooks/
tests/
characterization-tests/
golden-master-tests/
soap-contract-tests/
adapter-tests/
archunit-tests/
migration-tests/
smoke-tests/
docs/
architecture/
decisions/
diagrams/
migration/
operations/
risks/Artefaktstruktur
Das finale Gesamtpaket soll später so aufgebaut werden:
00_rohmaterial_wachsend/
projekt_rohmaterial_wachsend.zip
rohmaterial/
runde_01_roh_1zu1.md
runde_02_roh_1zu1.md
runde_03_roh_1zu1.md
...
index/
rohmaterial_index.md
manifest/
rohmaterial_manifest.json
01_lernbuch/
masterbook.md
pc.html
mobile.html
pc.pdf
mobile.pdf
02_beispielprojekt/
legacy-claims-customer-support-enterprise.zip
03_refactoring_changes/
refactoring_phases.zip
essential_changes.zip
changelog_v2.zip
before_after_mapping.zip
04_pruefberichte/
manifest.json
link_check.md
pdf_check.md
zip_check.md
structure_check.md
svg_check.md
artifact_manifest.json
05_gesamtpaket/
legacy_claims_customer_support_gesamtpaket.zipRefactoring-Phasen-Paket
Die Refactoring-Phasen sind später nicht nur Dokumentation, sondern didaktische Zwischenstände. Dadurch kann man sehen:
- Vorher-Zustand
- Sicherheitsnetz
- Zwischenschritte
- neue Architekturgrenzen
- neue Tests
- Migrationsnutzen
01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/Refactoring-Phasen
| Phase | Thema | Ziel |
|---|---|---|
| 01 | Inventory und Characterization | Verstehen, was wirklich existiert |
| 02 | Systemlandschaft und Zusatzsysteme erfassen | Integrationen und Abhängigkeiten sichtbar machen |
| 03 | Monster Method verstehen | processClaimDecision(...) analysieren |
| 04 | Facade und Command extrahieren | Use Case von EJB lösen |
| 05 | Statuslogik in State/Policy überführen | if/else-Ketten reduzieren |
| 06 | Coverage, Fraud, Document, Payment als Ports | externe Systeme entkoppeln |
| 07 | Customer und Identity als Ports | Stammdaten und Security entkoppeln |
| 08 | Workflow Orchestrator | Prozesslogik sichtbar machen |
| 09 | Audit Trail abstrahieren | fachliche Nachvollziehbarkeit sichern |
| 10 | Search, Notification, Reporting trennen | Nebenlogik aus Use Case lösen |
| 11 | Outbox und Idempotency | robuste Integration vorbereiten |
| 12 | Batch und SLA trennen | TimerBean ablösen |
| 13 | Portal und Backoffice entkoppeln | UI schrittweise modernisieren |
| 14 | OpenShift vorbereiten | Runtime-Grenzen und Konfiguration klären |
Essential-Changes-Paket
Das Essential-Changes-Paket soll später die wichtigsten Architekturänderungen fachlich nachvollziehbar machen.
01_change_catalog/
02_before_legacy_examples/
03_after_modular_examples/
04_tests_and_safety_net/
05_migration_changes/
06_original_project_zip/Change Catalog Struktur
Jede Änderung bekommt später:
- Änderung
- Vorher
- Nachher
- Pattern / Architekturkonzept
- relevante Ordner
- Test / Sicherheitsnetz
- Migrationsnutzen
Beispiel:
| Feld | Beispiel |
|---|---|
| Änderung | Monster Method zerlegt |
| Vorher | LegacyClaimFacadeBean.processClaimDecision(...) |
| Nachher | ProcessClaimDecisionHandler + Policies + Ports |
| Pattern | Command Handler, Application Service, Ports & Adapters |
| Ordner | before_refactoring_legacy,
after_refactoring_modular/claim-application |
| Test | Golden Master + Handler Test |
| Nutzen | Fachlogik wird ohne WebSphere testbar |
Changelog V2 Zielstruktur
Später werden erzeugt:
CHANGELOG_V2.md
CHANGELOG_V2_KURZFASSUNG.md
CHANGELOG_V2_MATRIX.csv
CHANGELOG_V2.jsonChangelog-Felder
| Feld | Bedeutung |
|---|---|
| ID | eindeutige Änderungs-ID |
| Kategorie | Refactoring, Migration, Tests, Security, Integration |
| Änderung | kurze Beschreibung |
| Vorher | Legacy-Zustand |
| Nachher | moderner Zustand |
| betroffene Module | Module und Ordner |
| Pattern / Architekturkonzept | z. B. State, Policy, Port, Adapter |
| Warum geändert? | fachlicher/technischer Grund |
| Migrationsnutzen | Nutzen für OpenShift/Modernisierung |
| Tests / Sicherheitsnetz | konkrete Tests |
| Risiko / Achtung | verbleibende Risiken |
Before/After-Mapping Zielstruktur
Später werden erzeugt:
BEFORE_AFTER_MAPPING.md
BEFORE_AFTER_MAPPING.csv
BEFORE_AFTER_MAPPING.json
BEFORE_AFTER_MAPPING_KURZFASSUNG.mdMapping-Grundidee
Legacy-Datei / Legacy-Modul
-> neue Datei / neues Modul
-> Grund der Änderung
-> Pattern / Architekturkonzept
-> Test / Sicherheitsnetz
-> MigrationsnutzenErste technische Modulgrenzen
Package-Regel
Alle Java-Beispiele verwenden:
package com.seb4u.demo.claims...Beispiele:
com.seb4u.demo.claims.domain
com.seb4u.demo.claims.application
com.seb4u.demo.claims.application.port
com.seb4u.demo.claims.infrastructure.soap
com.seb4u.demo.claims.infrastructure.jms
com.seb4u.demo.claims.infrastructure.persistence
com.seb4u.demo.claims.workflow
com.seb4u.demo.claims.document
com.seb4u.demo.claims.payment
com.seb4u.demo.claims.audit
com.seb4u.demo.claims.search
com.seb4u.demo.claims.notification
com.seb4u.demo.claims.securityModulabhängigkeiten als Regel
claim-domain
darf keine Abhängigkeit auf EJB, SOAP, JMS, JDBC, JPA, Servlet, Spring, Jakarta Runtime haben.
claim-application
darf domain verwenden.
definiert Ports.
darf keine konkreten SOAP/JMS/JDBC/File-Share-Implementierungen enthalten.
claim-infrastructure
implementiert Ports.
darf technische Frameworks verwenden.
claim-soap-api
adaptiert Legacy SOAP Requests auf Application Commands.
claim-rest-api
adaptiert moderne REST Requests auf Application Commands.
claim-batch
ruft Application Use Cases für Batch/SLA/Eskalation auf.
migration-tests
prüft Alt-/Neu-Verhalten, SOAP/REST-Kompatibilität und Smoke-Szenarien.Erste Code-Skelette
Diese Code-Skelette sind bewusst noch nicht vollständig. Sie legen die Grenzen fest, die in Runde 2 und Runde 3 mit Legacy-Code, Monster Method und Refactoring gefüllt werden.
Parent POM Grobstruktur
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Legacy Claims & Customer Support Enterprise System</name>
<modules>
<module>after_refactoring_modular/shared-kernel</module>
<module>after_refactoring_modular/claim-domain</module>
<module>after_refactoring_modular/claim-application</module>
<module>after_refactoring_modular/claim-document</module>
<module>after_refactoring_modular/claim-workflow</module>
<module>after_refactoring_modular/claim-payment</module>
<module>after_refactoring_modular/claim-audit</module>
<module>after_refactoring_modular/claim-search</module>
<module>after_refactoring_modular/claim-notification</module>
<module>after_refactoring_modular/claim-infrastructure</module>
<module>after_refactoring_modular/claim-soap-api</module>
<module>after_refactoring_modular/claim-rest-api</module>
<module>after_refactoring_modular/claim-batch</module>
<module>after_refactoring_modular/claim-openapi</module>
<module>after_refactoring_modular/migration-tests</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>Domain Package Boundary
package com.seb4u.demo.claims.domain;
/**
* Enthält reine Fachlogik für Claims.
*
* Verboten:
* - javax.ejb
* - jakarta.ejb
* - javax.xml.ws
* - jakarta.ws.rs
* - java.sql
* - JMS APIs
* - Servlet APIs
* - WebSphere-spezifische APIs
*
* Erlaubt:
* - Fachliche Entities
* - Value Objects
* - Policies
* - State Machine
* - Domain Events
* - Fachliche Exceptions, sofern sie keine technische Infrastruktur verstecken
*/
public final class DomainBoundary {
private DomainBoundary() {
}
}Application Port Beispiele
package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.DocumentId;
public interface DocumentStoragePort {
StoredDocumentReference storeClaimDocument(ClaimId claimId, UploadedClaimDocument document);
ClaimDocumentContent loadClaimDocument(ClaimId claimId, DocumentId documentId);
void archiveClaimDocument(ClaimId claimId, DocumentId documentId, ArchiveReason reason);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.RiskScore;
public interface FraudRiskPort {
RiskScore evaluateFraudRisk(Claim claim);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.CoverageDecision;
public interface CoveragePort {
CoverageDecision calculateCoverage(Claim claim);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.Money;
public interface PaymentReleasePort {
PaymentPreparationResult preparePayment(ClaimId claimId, Money amount);
PaymentReleaseResult releasePreparedPayment(ClaimId claimId, PaymentReference reference);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.AuditEvent;
public interface AuditLogPort {
void append(AuditEvent event);
ClaimAuditTrail loadTrail(ClaimId claimId);
}Command Handler als Zielrichtung
package com.seb4u.demo.claims.application.usecase;
import com.seb4u.demo.claims.application.port.AuditLogPort;
import com.seb4u.demo.claims.application.port.CoveragePort;
import com.seb4u.demo.claims.application.port.DocumentVerificationPort;
import com.seb4u.demo.claims.application.port.FraudRiskPort;
import com.seb4u.demo.claims.application.port.PaymentReleasePort;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.ClaimDecisionResult;
import com.seb4u.demo.claims.workflow.ClaimWorkflowOrchestrator;
public class ProcessClaimDecisionHandler {
private final ClaimRepository claimRepository;
private final CoveragePort coveragePort;
private final FraudRiskPort fraudRiskPort;
private final DocumentVerificationPort documentVerificationPort;
private final PaymentReleasePort paymentReleasePort;
private final AuditLogPort auditLogPort;
private final ClaimWorkflowOrchestrator workflowOrchestrator;
public ProcessClaimDecisionHandler(
ClaimRepository claimRepository,
CoveragePort coveragePort,
FraudRiskPort fraudRiskPort,
DocumentVerificationPort documentVerificationPort,
PaymentReleasePort paymentReleasePort,
AuditLogPort auditLogPort,
ClaimWorkflowOrchestrator workflowOrchestrator
) {
this.claimRepository = claimRepository;
this.coveragePort = coveragePort;
this.fraudRiskPort = fraudRiskPort;
this.documentVerificationPort = documentVerificationPort;
this.paymentReleasePort = paymentReleasePort;
this.auditLogPort = auditLogPort;
this.workflowOrchestrator = workflowOrchestrator;
}
public ClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
Claim claim = claimRepository.loadForDecision(command.claimId());
return workflowOrchestrator.processDecision(
claim,
command,
coveragePort,
fraudRiskPort,
documentVerificationPort,
paymentReleasePort,
auditLogPort
);
}
}Wichtig: In Runde 2 wird gezeigt, wie diese Struktur
im Legacy-System noch nicht existiert. Dort steckt alles in
LegacyClaimFacadeBean.processClaimDecision(...). In Runde 3
wird diese Zielrichtung Schritt für Schritt aus der
Legacy-Monster-Methode herausgearbeitet.
Legacy-Ausgangspunkt für Runde 2
Runde 2 soll den schlechten Ausgangszustand sichtbar machen.
Erwartete Legacy-Dateien
legacy-websphere-ear/
pom.xml
src/main/application/META-INF/application.xml
src/main/application/META-INF/ibm-application-bnd.xml
legacy-claims-ejb/
src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimFacadeBean.java
src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimDecisionServiceBean.java
src/main/java/com/seb4u/demo/claims/legacy/dao/LegacyClaimJdbcDao.java
src/main/java/com/seb4u/demo/claims/legacy/dao/AuditReportStoredProcedureDao.java
src/main/java/com/seb4u/demo/claims/legacy/soap/FraudRiskSoapClient.java
src/main/java/com/seb4u/demo/claims/legacy/soap/PolicyCoverageSoapClient.java
src/main/java/com/seb4u/demo/claims/legacy/jms/LegacyNotificationSenderBean.java
src/main/java/com/seb4u/demo/claims/legacy/security/JaasRoleChecker.java
legacy-claims-web/
src/main/webapp/WEB-INF/web.xml
src/main/webapp/WEB-INF/ibm-web-bnd.xml
src/main/webapp/claimDecision.jsp
src/main/java/com/seb4u/demo/claims/legacy/web/LegacyPartnerPortalServlet.java
src/main/java/com/seb4u/demo/claims/legacy/web/InternalClaimsBackofficeServlet.javaTypische Monster-Method-Zutaten
processClaimDecision(...) soll später bewusst
enthalten:
- Rollenprüfung mit JAAS
- Laden des Claims über JDBC
- Laden von Kundendaten über SOAP
- Coverage-Prüfung über SOAP oder Stored Procedure
- Fraud-Prüfung über SOAP
- Dokumentenprüfung über File Share und/oder SOAP
- Statusprüfung über if/else-Ketten
- Zahlungsvorbereitung
- Payment Release SOAP-Aufruf
- JMS-Benachrichtigung
- Audit Stored Procedure
- Oracle Updates
- technische Exception-Behandlung
- fachlich problematische Rollbacks
- harte Pfade
- harte Statuscodes
- harte Rollenstrings
- versteckte SLA-Logik
- Nebenwirkungen in falscher Reihenfolge
Teststrategie als Zielbild
Testpyramide für das Lernprojekt
+-----------------------------+
| wenige End-to-End Smoke |
+-----------------------------+
+-------------------------------+
| Migration / SOAP-REST Tests |
+-------------------------------+
+---------------------------------+
| Adapter / Contract Tests |
+---------------------------------+
+-----------------------------------+
| Application / Use Case Tests |
+-----------------------------------+
+-------------------------------------+
| Domain / Policy / State Tests |
+-------------------------------------+
+---------------------------------------+
| Characterization / Golden Master |
+---------------------------------------+Testarten
| Testart | Zweck | Zeitpunkt |
|---|---|---|
| Characterization Test | Legacy-Verhalten dokumentieren | vor Refactoring |
| Golden Master Test | bekannte Inputs/Outputs sichern | vor und während Refactoring |
| SOAP Contract Test | WSDL/Legacy-Kompatibilität sichern | während Migration |
| Application Test | Use Cases unabhängig testen | nach Extraktion |
| Domain Test | Policies/State Machine prüfen | nach Domain-Extraktion |
| Adapter Test | SOAP/JMS/JDBC/File Share isoliert testen | bei Port/Adapter |
| ArchUnit Test | Modulregeln absichern | nach Modularisierung |
| Migration Test | Alt/Neu vergleichen | bei OpenShift Vorbereitung |
| Smoke Test | Runtime grob prüfen | nach Deployment |
OpenShift-Zielbild
OpenShift ist in diesem Lernprojekt nicht nur eine technische Zielplattform. Es ist der Grund, warum Runtime-Grenzen sichtbar werden müssen.
Zu migrierende Runtime-Aspekte
| Legacy | OpenShift-Ziel |
|---|---|
| WebSphere Traditional EAR | Containerisierte Services |
| JNDI DataSources | externe Konfiguration / Secrets |
| WebSphere Scheduler / EJB Timer | OpenShift CronJob / Batch Worker |
| lokale File Shares | Adapter, später Object Storage möglich |
| manuelle Deployments | Build Pipeline / Image / DeploymentConfig oder Deployment |
| technische Logs | strukturierte Logs, Correlation IDs |
| WebSphere Rollen | Identity Provider, Security Policies |
| hardcodierte Properties | ConfigMap / Secret / Env Vars |
| direkte JMS-Nebenwirkungen | Outbox, Retry, Dead Letter |
| globale JTA-Annahmen | klare Transaktionsgrenzen, Compensation |
Zielordner für Migration
migration_to_openshift/
docker/
Dockerfile.openliberty
Dockerfile.springboot
openshift/
base/
deployment.yaml
service.yaml
route.yaml
configmap.yaml
secret.example.yaml
overlays/
dev/
test/
prod/
config/
application-dev.yaml
application-test.yaml
application-prod.yaml
runbooks/
deployment_runbook.md
rollback_runbook.md
incident_runbook.mdEntscheidungen in Runde 1
Entscheidung 1: Legacy und Modern getrennt halten
Der Legacy-Ausgangspunkt bleibt sichtbar in
before_refactoring_legacy/. Der moderne Zielzustand
entsteht in after_refactoring_modular/.
Warum? Damit man didaktisch vergleichen kann, was sich wirklich geändert hat.
Entscheidung 2: Refactoring-Schritte separat versionieren
Die Zwischenstände werden in refactoring_steps/
geführt.
Warum? Refactoring ist ein Lernprozess. Wenn nur Vorher und Nachher existieren, fehlt der wichtigste Teil.
Entscheidung 3: Zusatzsysteme bekommen eigene Legacy-Ordner
Alle Zusatzsysteme werden als eigene Legacy-Bausteine angelegt.
Warum? Im echten Enterprise-System liegen Probleme oft in Integrationen, nicht nur im Hauptsystem.
Entscheidung 4: Ports werden in Application bzw. Fachmodulen sichtbar gemacht
Externe Systeme werden über Ports angebunden.
Warum? Damit Fachlogik testbar wird und Infrastruktur später austauschbar ist.
Entscheidung 5: HTML später ohne Pflicht-JavaScript
Die HTML-Version wird später robust und offline-fähig gebaut.
Warum? Lokale HTML-Dateien auf iPhone/Safari können bei JavaScript-Buttons unzuverlässig sein.
033. Legacy-Ausgangssystem verstehen
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Legacy-Ausgangssystem verstehen in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Projekt: Legacy Claims & Customer Support Enterprise System Runde: 02 Datum: 2026-07-05 Arbeitsmodus: Rohmaterial, bewusst ausführlich, noch nicht konsolidiert Ziel dieser Runde: Das alte Ausgangssystem realistisch sichtbar machen, damit spätere Refactoring-, Test- und Migrationsschritte auf einem konkreten Legacy-Schnitt beruhen.
Einordnung dieser Runde
Runde 1 hat das Zielbild, die Systemlandschaft und die geplante Gesamtstruktur festgelegt. Runde 2 erzeugt jetzt den eigentlichen Legacy-Ausgangspunkt. Das ist wichtig, weil Refactoring nur dann didaktisch sinnvoll ist, wenn vorher sichtbar ist, wie schmutzig, gekoppelt und schwer testbar das alte System war.
Diese Runde ist daher absichtlich nicht „schön“. Viele Beispiele sind bewusst problematisch:
- zu große EJB-Fassade
- Fachlogik direkt im technischen Boundary-Code
- SOAP-Clients direkt aus Fachmethoden aufgerufen
- JDBC, Stored Procedures und JPA gemischt
- JMS direkt in derselben Transaktion publiziert
- Dokumentenpfade als String zusammengebaut
- JAAS- und LDAP-Rollenprüfung mitten im Ablauf
- Statusübergänge als
if/else-Ketten - technische Exceptions steuern Fachfluss
- Audit-Logik versteckt in mehreren Stellen
- TimerBeans manipulieren Claims direkt
- Partner Portal und Backoffice sind direkt an Legacy-EJBs gekoppelt
Das Ziel ist nicht, diesen Code zu empfehlen. Das Ziel ist, ein realistisches Ausgangsmaterial zu haben, das wir in späteren Runden Schritt für Schritt retten.
Legacy-Gesamtbild vor Refactoring
Technische Sicht
Das alte System läuft auf:
- IBM WebSphere Traditional
- Java 8
- Java EE 7
- EAR Deployment
- WAR Module für Partner Portal und Backoffice
- EJB-JAR für zentrale Claims-Fassade
- SOAP Web Services für externe Partner und interne Altsysteme
- SOAP Clients zu Customer, Policy, Fraud, Document, Payment und Notification
- JTA für globale Transaktionen
- JMS über IBM MQ
- Oracle DB
- Stored Procedures für Statusänderungen, Reports und Batchjobs
- File Shares für Dokumente
- LDAP / JAAS / WebSphere Rollen
- WebSphere Timer / Scheduler
Fachliche Sicht
Das System verarbeitet Schadensfälle und Support-Workflows:
- Partner oder Sachbearbeiter erstellt Schaden.
- Dokumente werden hochgeladen.
- Kunde und Police werden geprüft.
- Betrugsrisiko wird bewertet.
- Dokumente werden technisch und fachlich verifiziert.
- Deckung und Selbstbehalt werden berechnet.
- Entscheidung wird vorbereitet.
- Teamleiter oder Spezialist genehmigt.
- Zahlung wird freigegeben.
- Kunde, Partner und interne Stellen werden benachrichtigt.
- Audit Trail, Reporting, Suche und SLA-Verarbeitung laufen parallel.
Kritischer Legacy-Kern
Der problematische Kern ist die Methode:
processClaimDecision(...)Diese Methode macht fast alles:
- lädt Claim
- prüft Benutzerrolle
- prüft Status
- ruft Customer Master Data SOAP Service
- ruft Policy Coverage SOAP Service
- ruft Fraud Risk SOAP Service
- prüft Dokumente über File Share und SOAP
- ruft Stored Procedures
- aktualisiert Status
- schreibt Audit
- sendet JMS Events
- ruft Payment SOAP Service
- sendet Notification
- triggert SLA/Eskalation
- behandelt Fehler mit technischen Exceptions
Diese Methode wird in dieser Runde bewusst als Monster Method gezeigt.
Legacy-EAR-Struktur
Die alte Anwendung ist als EAR aufgebaut.
legacy-claims-customer-support-ear/
├── pom.xml
├── src/main/application/META-INF/
│ ├── application.xml
│ ├── ibm-application-bnd.xml
│ └── was.policy
├── legacy-claims-core-ejb.jar
├── legacy-claims-partner-portal.war
├── legacy-internal-claims-backoffice.war
├── legacy-document-management-adapter.jar
├── legacy-fraud-risk-gateway.jar
├── legacy-policy-coverage-system.jar
├── legacy-payment-release-system.jar
├── legacy-notification-system.jar
├── legacy-sla-escalation-scheduler.jar
├── legacy-audit-compliance-reporter.jar
├── legacy-search-indexer.jar
├── legacy-customer-master-data-system.jar
└── legacy-identity-role-management.jarLegacy-Core-EJB-Struktur
legacy-claims-core-ejb/
├── src/main/java/com/seb4u/demo/claims/legacy/ejb/
│ ├── LegacyClaimFacadeBean.java
│ ├── LegacyClaimQueryBean.java
│ ├── LegacyClaimDocumentBean.java
│ ├── LegacyClaimPaymentBean.java
│ └── LegacyClaimAuditBean.java
├── src/main/java/com/seb4u/demo/claims/legacy/dao/
│ ├── LegacyClaimDao.java
│ ├── LegacyClaimSearchDao.java
│ ├── AuditReportStoredProcedureDao.java
│ └── LegacySequenceDao.java
├── src/main/java/com/seb4u/demo/claims/legacy/soap/
│ ├── LegacyClaimSoapEndpoint.java
│ ├── CustomerMasterDataSoapClient.java
│ ├── PolicyCoverageSoapClient.java
│ ├── FraudRiskSoapClient.java
│ ├── DocumentVerificationSoapClient.java
│ ├── PaymentReleaseSoapClient.java
│ └── NotificationSoapClient.java
├── src/main/java/com/seb4u/demo/claims/legacy/jms/
│ ├── LegacyClaimEventPublisher.java
│ └── LegacyClaimEventMessage.java
├── src/main/java/com/seb4u/demo/claims/legacy/security/
│ ├── JaasRoleChecker.java
│ └── LegacyLdapGroupResolver.java
├── src/main/java/com/seb4u/demo/claims/legacy/document/
│ ├── LegacyDocumentShareClient.java
│ └── LegacyDocumentPathBuilder.java
├── src/main/java/com/seb4u/demo/claims/legacy/scheduler/
│ └── SlaEscalationTimerBean.java
└── src/main/resources/META-INF/
├── ejb-jar.xml
├── ibm-ejb-jar-bnd.xml
└── persistence.xmlPartner-Portal-WAR-Struktur
legacy-claims-partner-portal/
├── src/main/java/com/seb4u/demo/claims/legacy/portal/
│ ├── LegacyPartnerPortalServlet.java
│ ├── PartnerClaimUploadServlet.java
│ ├── PartnerClaimStatusServlet.java
│ └── PartnerPortalSessionFilter.java
├── src/main/webapp/
│ ├── WEB-INF/web.xml
│ ├── WEB-INF/ibm-web-bnd.xml
│ ├── WEB-INF/jsp/createClaim.jsp
│ ├── WEB-INF/jsp/uploadDocument.jsp
│ ├── WEB-INF/jsp/claimStatus.jsp
│ └── WEB-INF/jsp/messages.jsp
└── pom.xmlInternal-Backoffice-WAR-Struktur
legacy-internal-claims-backoffice/
├── src/main/java/com/seb4u/demo/claims/legacy/backoffice/
│ ├── InternalClaimsBackofficeServlet.java
│ ├── InternalClaimsBackofficeBean.java
│ ├── BackofficeSearchServlet.java
│ ├── BackofficeDecisionServlet.java
│ └── BackofficeSecurityFilter.java
├── src/main/webapp/
│ ├── WEB-INF/web.xml
│ ├── WEB-INF/ibm-web-bnd.xml
│ ├── WEB-INF/jsp/searchClaims.jsp
│ ├── WEB-INF/jsp/editClaim.jsp
│ ├── WEB-INF/jsp/reviewDocuments.jsp
│ ├── WEB-INF/jsp/decision.jsp
│ └── WEB-INF/jsp/auditTrail.jsp
└── pom.xmlWebSphere-Deskriptoren
application.xml
<application xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/application_7.xsd"
version="7">
<display-name>Legacy Claims Customer Support EAR</display-name>
<module>
<ejb>legacy-claims-core-ejb.jar</ejb>
</module>
<module>
<web>
<web-uri>legacy-claims-partner-portal.war</web-uri>
<context-root>/partner-claims</context-root>
</web>
</module>
<module>
<web>
<web-uri>legacy-internal-claims-backoffice.war</web-uri>
<context-root>/claims-backoffice</context-root>
</web>
</module>
<module>
<ejb>legacy-sla-escalation-scheduler.jar</ejb>
</module>
<library-directory>lib</library-directory>
</application>Problematisch daran:
- Partner Portal und Backoffice hängen im gleichen Deployment wie die Fachlogik.
- Jede Änderung an einer JSP kann zu einem großen EAR-Rollout führen.
- Die technische Laufzeitgrenze ist nicht gleich der fachlichen Grenze.
- Externe Systemadapter sind nicht sauber als Ports modelliert.
ibm-application-bnd.xml
<application-bnd
xmlns="http://websphere.ibm.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://websphere.ibm.com/xml/ns/javaee
http://websphere.ibm.com/xml/ns/javaee/ibm-application-bnd_1_0.xsd"
version="1.0">
<security-role name="CLAIMS_AGENT">
<group name="cn=claims-agents,ou=groups,dc=seb4u,dc=demo" />
</security-role>
<security-role name="CLAIMS_MANAGER">
<group name="cn=claims-managers,ou=groups,dc=seb4u,dc=demo" />
</security-role>
<security-role name="PARTNER_USER">
<group name="cn=partner-users,ou=groups,dc=seb4u,dc=demo" />
</security-role>
<security-role name="AUDITOR">
<group name="cn=claims-auditors,ou=groups,dc=seb4u,dc=demo" />
</security-role>
</application-bnd>Problematisch daran:
- Rollen sind direkt an LDAP-Gruppen gebunden.
- Rollenlogik ist zusätzlich im Java-Code hart kodiert.
- Es gibt keine zentrale
AuthorizationPolicy. - Für OpenShift/Identity-Provider-Migration muss diese Bindung später aufgelöst werden.
ejb-jar.xml
<ejb-jar xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/ejb-jar_3_2.xsd"
version="3.2">
<enterprise-beans>
<session>
<ejb-name>LegacyClaimFacadeBean</ejb-name>
<ejb-class>com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
<assembly-descriptor>
<method-permission>
<role-name>CLAIMS_AGENT</role-name>
<role-name>CLAIMS_MANAGER</role-name>
<method>
<ejb-name>LegacyClaimFacadeBean</ejb-name>
<method-name>*</method-name>
</method>
</method-permission>
<container-transaction>
<method>
<ejb-name>LegacyClaimFacadeBean</ejb-name>
<method-name>processClaimDecision</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>Problematisch daran:
- Die gesamte Monster-Methode läuft in einer großen JTA-Transaktion.
- Externe SOAP-Aufrufe liegen innerhalb fachlicher Transaktionslogik.
- JMS-Publizierung und DB-Update sind eng vermischt.
- Fehler führen zu Rollbacks, obwohl manche externen Systeme bereits Seiteneffekte hatten.
persistence.xml
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="claimsLegacyPU" transaction-type="JTA">
<jta-data-source>jdbc/ClaimsOracleDS</jta-data-source>
<class>com.seb4u.demo.claims.legacy.entity.LegacyClaimEntity</class>
<class>com.seb4u.demo.claims.legacy.entity.LegacyClaimDocumentEntity</class>
<class>com.seb4u.demo.claims.legacy.entity.LegacyClaimAuditEntity</class>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
<property name="hibernate.show_sql" value="false" />
<property name="hibernate.format_sql" value="false" />
</properties>
</persistence-unit>
</persistence>Problematisch daran:
- JPA wird verwendet, aber Stored Procedures und JDBC laufen parallel.
- Es ist unklar, welche Daten über EntityManager und welche über JDBC verändert werden.
- Das erschwert Tests, Transaktionsverständnis und Migration.
Legacy-Datenbankmodell und Stored Procedures
Wichtige Tabellen
CLAIM
CLAIM_DOCUMENT
CLAIM_AUDIT
CLAIM_STATUS_HISTORY
CLAIM_PAYMENT
CLAIM_ESCALATION
CLAIM_MESSAGE
CLAIM_SEARCH_CACHE
CLAIM_REPORTING_SNAPSHOT
PARTNER_ACCESS
CLAIM_ROLE_ASSIGNMENTTypische Stored Procedures
CREATE OR REPLACE PROCEDURE SP_PROCESS_CLAIM_DECISION (
p_claim_id IN NUMBER,
p_decision_code IN VARCHAR2,
p_actor_user IN VARCHAR2,
p_reason IN VARCHAR2,
p_new_status OUT VARCHAR2,
p_audit_id OUT NUMBER
) AS
BEGIN
UPDATE CLAIM
SET STATUS = CASE
WHEN p_decision_code = 'APPROVE' THEN 'APPROVED'
WHEN p_decision_code = 'REJECT' THEN 'REJECTED'
WHEN p_decision_code = 'ESCALATE' THEN 'ESCALATED'
ELSE STATUS
END,
LAST_UPDATED_AT = SYSTIMESTAMP,
LAST_UPDATED_BY = p_actor_user
WHERE CLAIM_ID = p_claim_id;
INSERT INTO CLAIM_AUDIT (
AUDIT_ID,
CLAIM_ID,
ACTION_CODE,
ACTOR_USER,
DESCRIPTION,
CREATED_AT
) VALUES (
CLAIM_AUDIT_SEQ.NEXTVAL,
p_claim_id,
p_decision_code,
p_actor_user,
p_reason,
SYSTIMESTAMP
) RETURNING AUDIT_ID INTO p_audit_id;
SELECT STATUS INTO p_new_status
FROM CLAIM
WHERE CLAIM_ID = p_claim_id;
END;
/Legacy-Problem:
- Statuslogik steckt teilweise in Java und teilweise in PL/SQL.
- Audit entsteht teilweise in Java und teilweise in PL/SQL.
- Refactoring muss zuerst charakterisieren, was wirklich passiert.
Reporting Stored Procedure
CREATE OR REPLACE PROCEDURE SP_REFRESH_CLAIM_REPORTING AS
BEGIN
DELETE FROM CLAIM_REPORTING_SNAPSHOT;
INSERT INTO CLAIM_REPORTING_SNAPSHOT (
CLAIM_ID,
CUSTOMER_ID,
STATUS,
PARTNER_ID,
CLAIM_AMOUNT,
RISK_SCORE,
SLA_BREACHED,
LAST_ACTION,
SNAPSHOT_CREATED_AT
)
SELECT c.CLAIM_ID,
c.CUSTOMER_ID,
c.STATUS,
c.PARTNER_ID,
c.CLAIM_AMOUNT,
c.RISK_SCORE,
CASE WHEN e.ESCALATION_ID IS NOT NULL THEN 'Y' ELSE 'N' END,
a.ACTION_CODE,
SYSTIMESTAMP
FROM CLAIM c
LEFT JOIN CLAIM_ESCALATION e ON e.CLAIM_ID = c.CLAIM_ID
LEFT JOIN CLAIM_AUDIT a ON a.CLAIM_ID = c.CLAIM_ID
WHERE c.CREATED_AT > SYSTIMESTAMP - 365;
END;
/Legacy-Problem:
- Reporting ist kein eigener fachlicher Read Model Boundery.
- Performance-Probleme werden durch nächtliche Komplettrefreshs versteckt.
- Audits und Reports greifen auf dieselben Tabellen, aber mit unterschiedlicher Semantik.
Legacy-SOAP-Operationen
Interne SOAP-Fassade
Das alte System bietet eine SOAP-Fassade für Partner und andere Legacy-Systeme.
package com.seb4u.demo.claims.legacy.soap;
import javax.ejb.EJB;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
@WebService(
serviceName = "LegacyClaimService",
portName = "LegacyClaimServicePort",
targetNamespace = "http://legacy.claims.demo.seb4u.com"
)
public class LegacyClaimSoapEndpoint {
@EJB
private LegacyClaimFacadeBean claimFacade;
@WebMethod(operationName = "createClaim")
public LegacyCreateClaimResponse createClaim(@WebParam(name = "request") LegacyCreateClaimRequest request) {
return claimFacade.createClaimFromSoap(request);
}
@WebMethod(operationName = "getClaimStatus")
public LegacyClaimStatusResponse getClaimStatus(@WebParam(name = "request") LegacyClaimStatusRequest request) {
return claimFacade.getClaimStatus(request);
}
@WebMethod(operationName = "submitClaimDocument")
public LegacySubmitDocumentResponse submitClaimDocument(@WebParam(name = "request") LegacySubmitDocumentRequest request) {
return claimFacade.submitClaimDocument(request);
}
@WebMethod(operationName = "processClaimDecision")
public LegacyClaimDecisionResponse processClaimDecision(@WebParam(name = "request") LegacyClaimDecisionRequest request) {
return claimFacade.processClaimDecision(request);
}
@WebMethod(operationName = "approveClaimPayment")
public LegacyPaymentResponse approveClaimPayment(@WebParam(name = "request") LegacyPaymentRequest request) {
return claimFacade.approveClaimPayment(request);
}
@WebMethod(operationName = "rejectClaim")
public LegacyRejectClaimResponse rejectClaim(@WebParam(name = "request") LegacyRejectClaimRequest request) {
return claimFacade.rejectClaim(request);
}
@WebMethod(operationName = "escalateClaim")
public LegacyEscalationResponse escalateClaim(@WebParam(name = "request") LegacyEscalationRequest request) {
return claimFacade.escalateClaim(request);
}
@WebMethod(operationName = "getClaimAuditTrail")
public LegacyAuditTrailResponse getClaimAuditTrail(@WebParam(name = "request") LegacyAuditTrailRequest request) {
return claimFacade.getClaimAuditTrail(request);
}
@WebMethod(operationName = "searchClaims")
public LegacySearchClaimsResponse searchClaims(@WebParam(name = "request") LegacySearchClaimsRequest request) {
return claimFacade.searchClaims(request);
}
@WebMethod(operationName = "getCustomerClaimHistory")
public LegacyCustomerHistoryResponse getCustomerClaimHistory(@WebParam(name = "request") LegacyCustomerHistoryRequest request) {
return claimFacade.getCustomerClaimHistory(request);
}
@WebMethod(operationName = "verifyClaimDocument")
public LegacyVerifyDocumentResponse verifyClaimDocument(@WebParam(name = "request") LegacyVerifyDocumentRequest request) {
return claimFacade.verifyClaimDocument(request);
}
@WebMethod(operationName = "calculateClaimCoverage")
public LegacyCoverageResponse calculateClaimCoverage(@WebParam(name = "request") LegacyCoverageRequest request) {
return claimFacade.calculateClaimCoverage(request);
}
}Problematisch daran:
- SOAP Boundary ruft direkt die große EJB-Fassade.
- Kein separater Application Use Case.
- Request/Response-Objekte werden teilweise tief in der Fachlogik verwendet.
- Spätere REST/OpenAPI-Schicht kann nicht sauber wiederverwenden.
JMS und IBM MQ im Legacy-System
JMS Publisher
package com.seb4u.demo.claims.legacy.jms;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
@Stateless
public class LegacyClaimEventPublisher {
@Resource(lookup = "jms/ClaimsConnectionFactory")
private ConnectionFactory connectionFactory;
@Resource(lookup = "jms/ClaimsEventsQueue")
private Queue claimsEventsQueue;
public void publishDecisionChanged(Long claimId, String oldStatus, String newStatus, String actor, String correlationId) {
Connection connection = null;
Session session = null;
try {
connection = connectionFactory.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(claimsEventsQueue);
String payload = "{" +
"\"eventType\":\"CLAIM_DECISION_CHANGED\"," +
"\"claimId\":" + claimId + "," +
"\"oldStatus\":\"" + oldStatus + "\"," +
"\"newStatus\":\"" + newStatus + "\"," +
"\"actor\":\"" + actor + "\"," +
"\"correlationId\":\"" + correlationId + "\"" +
"}";
TextMessage message = session.createTextMessage(payload);
message.setStringProperty("eventType", "CLAIM_DECISION_CHANGED");
message.setStringProperty("correlationId", correlationId);
producer.send(message);
} catch (JMSException e) {
throw new IllegalStateException("Could not publish claim event", e);
} finally {
try {
if (session != null) session.close();
if (connection != null) connection.close();
} catch (JMSException ignored) {
// ignored in legacy system
}
}
}
}Legacy-Probleme:
- JSON wird per String-Konkatentation erzeugt.
- Kein Outbox Pattern.
- Kein Idempotency Key.
- Event-Publishing ist Teil des synchronen Workflows.
- Fehler werden als Runtime Exception weitergeworfen.
- Nachricht kann fehlen, obwohl DB geändert wurde, oder DB kann rollbacken, obwohl ein externes System bereits reagiert hat.
WebSphere JMS Bindings
<ejb-jar-bnd xmlns="http://websphere.ibm.com/xml/ns/javaee"
version="1.0">
<message-destination-ref name="jms/ClaimsEventsQueue"
binding-name="jms/ClaimsEventsQueue" />
<resource-ref name="jms/ClaimsConnectionFactory"
binding-name="jms/ClaimsConnectionFactory" />
</ejb-jar-bnd>Security: LDAP, JAAS und hart kodierte Rollen
JaasRoleChecker.java
package com.seb4u.demo.claims.legacy.security;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import java.util.HashSet;
import java.util.Set;
@Stateless
public class JaasRoleChecker {
@Resource
private SessionContext sessionContext;
public boolean canProcessDecision(String claimStatus, String decisionCode, String amountCategory) {
if (sessionContext.isCallerInRole("CLAIMS_MANAGER")) {
return true;
}
if (sessionContext.isCallerInRole("CLAIMS_AGENT")) {
if ("LOW".equals(amountCategory) && "APPROVE".equals(decisionCode)) {
return true;
}
if ("REJECT".equals(decisionCode) && ("NEW".equals(claimStatus) || "IN_REVIEW".equals(claimStatus))) {
return true;
}
}
if (sessionContext.isCallerInRole("PARTNER_USER")) {
return false;
}
return false;
}
public Set<String> currentRoles() {
Set<String> roles = new HashSet<>();
if (sessionContext.isCallerInRole("CLAIMS_AGENT")) roles.add("CLAIMS_AGENT");
if (sessionContext.isCallerInRole("CLAIMS_MANAGER")) roles.add("CLAIMS_MANAGER");
if (sessionContext.isCallerInRole("PARTNER_USER")) roles.add("PARTNER_USER");
if (sessionContext.isCallerInRole("AUDITOR")) roles.add("AUDITOR");
return roles;
}
}Legacy-Probleme:
- Security-Entscheidung ist nicht als fachliche Policy modelliert.
- Rollennamen sind überall im Code verteilt.
- Kein Test ohne Container.
- Keine klare Migration zu OIDC/Keycloak/Identity Provider möglich.
Document Management Adapter im Legacy-System
LegacyDocumentShareClient.java
package com.seb4u.demo.claims.legacy.document;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class LegacyDocumentShareClient {
private static final String ROOT = "//legacy-fileserver01/claims-documents";
public String storeDocument(Long claimId, String documentType, String fileName, File uploadedFile) {
String folder = ROOT + "/" + claimId + "/" + documentType;
File directory = new File(folder);
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalStateException("Could not create document folder " + folder);
}
String targetPath = folder + "/" + System.currentTimeMillis() + "_" + fileName;
try {
Files.copy(uploadedFile.toPath(), Path.of(targetPath), StandardCopyOption.REPLACE_EXISTING);
return targetPath;
} catch (IOException e) {
throw new IllegalStateException("Could not store document " + fileName, e);
}
}
public byte[] readDocument(String absolutePath) {
try (FileInputStream in = new FileInputStream(absolutePath)) {
return in.readAllBytes();
} catch (IOException e) {
throw new IllegalStateException("Could not read document " + absolutePath, e);
}
}
public boolean exists(String absolutePath) {
return new File(absolutePath).exists();
}
}Legacy-Probleme:
- Dokumentenablage ist hart verdrahtet.
- Absolute Pfade landen in der Datenbank.
- Keine saubere Domänenabstraktion.
- Kein Virenscan-/Dokumentverifikationsmodell.
- Kein Objekt-Storage-Migrationspfad.
Legacy-DAO mit JDBC, JPA und Stored Procedures
LegacyClaimDao.java
package com.seb4u.demo.claims.legacy.dao;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.sql.DataSource;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
@Stateless
public class LegacyClaimDao {
@PersistenceContext(unitName = "claimsLegacyPU")
private EntityManager entityManager;
@Resource(lookup = "jdbc/ClaimsOracleDS")
private DataSource dataSource;
public LegacyClaimRecord findByIdForUpdate(Long claimId) {
try (Connection connection = dataSource.getConnection();
CallableStatement statement = connection.prepareCall(
"select CLAIM_ID, STATUS, CUSTOMER_ID, POLICY_NO, CLAIM_AMOUNT, RISK_SCORE, " +
" PARTNER_ID, DOCUMENT_STATUS, PAYMENT_STATUS, SLA_DUE_AT " +
" from CLAIM where CLAIM_ID = ? for update")) {
statement.setLong(1, claimId);
try (ResultSet rs = statement.executeQuery()) {
if (!rs.next()) {
return null;
}
return new LegacyClaimRecord(
rs.getLong("CLAIM_ID"),
rs.getString("STATUS"),
rs.getString("CUSTOMER_ID"),
rs.getString("POLICY_NO"),
rs.getBigDecimal("CLAIM_AMOUNT"),
rs.getBigDecimal("RISK_SCORE"),
rs.getString("PARTNER_ID"),
rs.getString("DOCUMENT_STATUS"),
rs.getString("PAYMENT_STATUS"),
rs.getTimestamp("SLA_DUE_AT")
);
}
} catch (SQLException e) {
throw new IllegalStateException("Could not load claim " + claimId, e);
}
}
public StoredProcedureDecisionResult callProcessClaimDecision(Long claimId, String decisionCode, String actor, String reason) {
try (Connection connection = dataSource.getConnection();
CallableStatement statement = connection.prepareCall("{call SP_PROCESS_CLAIM_DECISION(?,?,?,?,?,?)}")) {
statement.setLong(1, claimId);
statement.setString(2, decisionCode);
statement.setString(3, actor);
statement.setString(4, reason);
statement.registerOutParameter(5, Types.VARCHAR);
statement.registerOutParameter(6, Types.NUMERIC);
statement.execute();
return new StoredProcedureDecisionResult(
statement.getString(5),
statement.getLong(6)
);
} catch (SQLException e) {
throw new IllegalStateException("Could not call SP_PROCESS_CLAIM_DECISION", e);
}
}
public void updateRiskScore(Long claimId, BigDecimal riskScore) {
entityManager.createNativeQuery(
"update CLAIM set RISK_SCORE = :riskScore, LAST_UPDATED_AT = SYSTIMESTAMP where CLAIM_ID = :claimId")
.setParameter("riskScore", riskScore)
.setParameter("claimId", claimId)
.executeUpdate();
}
public void markDocumentStatus(Long claimId, String documentStatus) {
entityManager.createNativeQuery(
"update CLAIM set DOCUMENT_STATUS = :status where CLAIM_ID = :claimId")
.setParameter("status", documentStatus)
.setParameter("claimId", claimId)
.executeUpdate();
}
}Legacy-Probleme:
prepareCallwird für SELECT missbraucht.- JPA und JDBC werden gemischt.
- Transaktionale Grenzen sind unklar.
- Native Queries verteilen Fachstatuswerte.
- Kein Repository mit fachlicher Sprache.
Legacy Partner Portal
LegacyPartnerPortalServlet.java
package com.seb4u.demo.claims.legacy.portal;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.soap.LegacyCreateClaimRequest;
import com.seb4u.demo.claims.legacy.soap.LegacyCreateClaimResponse;
import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@MultipartConfig
public class LegacyPartnerPortalServlet extends HttpServlet {
@EJB
private LegacyClaimFacadeBean claimFacade;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String partnerId = (String) request.getSession().getAttribute("partnerId");
String customerId = request.getParameter("customerId");
String policyNo = request.getParameter("policyNo");
String incidentText = request.getParameter("incidentText");
String amount = request.getParameter("claimAmount");
LegacyCreateClaimRequest legacyRequest = new LegacyCreateClaimRequest();
legacyRequest.setPartnerId(partnerId);
legacyRequest.setCustomerId(customerId);
legacyRequest.setPolicyNo(policyNo);
legacyRequest.setIncidentDescription(incidentText);
legacyRequest.setClaimAmount(amount);
legacyRequest.setSourceSystem("PARTNER_PORTAL_JSP");
try {
LegacyCreateClaimResponse legacyResponse = claimFacade.createClaimFromSoap(legacyRequest);
request.setAttribute("claimId", legacyResponse.getClaimId());
request.setAttribute("message", "Schaden wurde erfasst");
request.getRequestDispatcher("/WEB-INF/jsp/claimStatus.jsp").forward(request, response);
} catch (Exception ex) {
request.setAttribute("error", ex.getMessage());
request.getRequestDispatcher("/WEB-INF/jsp/createClaim.jsp").forward(request, response);
}
}
}Legacy-Probleme:
- Servlet baut SOAP-Request-Objekte, obwohl es intern eine EJB nutzt.
- JSP/UI ist an Legacy-Request-Modelle gekoppelt.
- Fehlertexte aus Exceptions werden direkt angezeigt.
- Keine saubere Partner-Portal-API.
- Keine BFF-Schicht.
Legacy Internal Claims Backoffice
InternalClaimsBackofficeBean.java
package com.seb4u.demo.claims.legacy.backoffice;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.soap.LegacyClaimDecisionRequest;
import com.seb4u.demo.claims.legacy.soap.LegacyClaimDecisionResponse;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import java.util.Map;
@Stateless
public class InternalClaimsBackofficeBean {
@EJB
private LegacyClaimFacadeBean claimFacade;
public LegacyClaimDecisionResponse submitDecisionForm(Map<String, String> form, String currentUser) {
LegacyClaimDecisionRequest request = new LegacyClaimDecisionRequest();
request.setClaimId(Long.valueOf(form.get("claimId")));
request.setDecisionCode(form.get("decisionCode"));
request.setReason(form.get("reason"));
request.setActorUser(currentUser);
request.setForcePayment("on".equals(form.get("forcePayment")));
request.setManualOverride("on".equals(form.get("manualOverride")));
request.setBackofficeComment(form.get("comment"));
request.setCorrelationId(form.get("correlationId"));
return claimFacade.processClaimDecision(request);
}
}Legacy-Probleme:
- Backoffice nutzt die gleiche SOAP-Request-Struktur wie externe Partner.
- Manuelle Override-Flags fließen direkt in die Monster-Methode.
- Keine Use-Case-Klasse wie
ClaimBackofficeUseCase.
Legacy Zusatzsysteme als konkrete technische Adapter
FraudRiskSoapClient.java
package com.seb4u.demo.claims.legacy.soap;
import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;
public class FraudRiskSoapClient {
@WebServiceRef(wsdlLocation = "META-INF/wsdl/FraudRiskService.wsdl")
private FraudRiskService fraudRiskService;
public FraudRiskLegacyResponse checkRisk(Long claimId, String customerId, String policyNo, BigDecimal amount) {
FraudRiskLegacyRequest request = new FraudRiskLegacyRequest();
request.setClaimId(String.valueOf(claimId));
request.setCustomerId(customerId);
request.setPolicyNumber(policyNo);
request.setClaimAmount(amount.toPlainString());
request.setRequestMode("FULL_SYNC_CHECK");
request.setCaller("LEGACY_CLAIMS_EJB");
try {
return fraudRiskService.getFraudRiskServicePort().checkRisk(request);
} catch (Exception e) {
FraudRiskLegacyResponse fallback = new FraudRiskLegacyResponse();
fallback.setScore("999");
fallback.setRiskCategory("TECHNICAL_ERROR_TREATED_AS_HIGH_RISK");
fallback.setManualReviewRequired(true);
fallback.setMessage(e.getMessage());
return fallback;
}
}
}Problem:
- Technischer Fehler wird als fachlich hohes Risiko modelliert.
- Kein Timeout-Konzept im Code sichtbar.
- Kein Circuit Breaker.
- Kein Anti-Corruption Layer.
PolicyCoverageSoapClient.java
package com.seb4u.demo.claims.legacy.soap;
import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;
public class PolicyCoverageSoapClient {
@WebServiceRef(wsdlLocation = "META-INF/wsdl/PolicyCoverageService.wsdl")
private PolicyCoverageService policyCoverageService;
public LegacyCoverageResult calculateCoverage(String policyNo, String claimType, BigDecimal amount) {
LegacyCoverageRequest request = new LegacyCoverageRequest();
request.setPolicyNumber(policyNo);
request.setClaimType(claimType);
request.setRequestedAmount(amount.toPlainString());
request.setLegacyTariffMode("Y");
return policyCoverageService.getPolicyCoverageServicePort().calculateClaimCoverage(request);
}
}Problem:
- Coverage-Regeln bleiben als externer SOAP-Vertrag in der Fachlogik sichtbar.
- Kein
CoverageDecisionResult Object. - Keine
SpecificationoderCoveragePolicy.
PaymentReleaseSoapClient.java
package com.seb4u.demo.claims.legacy.soap;
import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;
public class PaymentReleaseSoapClient {
@WebServiceRef(wsdlLocation = "META-INF/wsdl/PaymentReleaseService.wsdl")
private PaymentReleaseService paymentReleaseService;
public LegacyPaymentReleaseResponse releasePayment(Long claimId, String customerId, BigDecimal amount, String actor) {
LegacyPaymentReleaseRequest request = new LegacyPaymentReleaseRequest();
request.setClaimReference("CLAIM-" + claimId);
request.setCustomerNumber(customerId);
request.setAmount(amount.toPlainString());
request.setCurrency("EUR");
request.setReleasedBy(actor);
request.setReason("CLAIM_APPROVED");
return paymentReleaseService.getPaymentReleaseServicePort().releasePayment(request);
}
}Problem:
- Zahlung wird synchron aus der Entscheidung heraus freigegeben.
- Kein Idempotency Key.
- Keine Compensation.
- Kein Outbox Pattern.
Die Monster Method: LegacyClaimFacadeBean.processClaimDecision(...)
Claims-Lebenszyklus hinter der Monster Method
Die lange Legacy-Methode vermischt mehrere fachliche Schritte. Der Lebenszyklus macht sichtbar, welche Entscheidungen später getrennt getestet und modularisiert werden.
- Erfassung und Plausibilitätsprüfung sind von der Entscheidung zu trennen.
- Zahlung, DMS und Benachrichtigung sind nachgelagerte Integrationen.
- Audit und SLA begleiten den Prozess, ersetzen aber keine Domain-Regel.
Die folgende Methode ist absichtlich lang und schlecht. Sie ist das zentrale Refactoring-Objekt für die nächsten Runden.
LegacyClaimFacadeBean.java
package com.seb4u.demo.claims.legacy.ejb;
import com.seb4u.demo.claims.legacy.dao.LegacyClaimDao;
import com.seb4u.demo.claims.legacy.dao.LegacyClaimRecord;
import com.seb4u.demo.claims.legacy.dao.StoredProcedureDecisionResult;
import com.seb4u.demo.claims.legacy.document.LegacyDocumentShareClient;
import com.seb4u.demo.claims.legacy.jms.LegacyClaimEventPublisher;
import com.seb4u.demo.claims.legacy.security.JaasRoleChecker;
import com.seb4u.demo.claims.legacy.soap.*;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Stateless
public class LegacyClaimFacadeBean {
@EJB
private LegacyClaimDao claimDao;
@EJB
private LegacyClaimAuditBean auditBean;
@EJB
private LegacyClaimDocumentBean documentBean;
@EJB
private LegacyClaimPaymentBean paymentBean;
@EJB
private JaasRoleChecker roleChecker;
@EJB
private LegacyClaimEventPublisher eventPublisher;
@EJB
private LegacyClaimSearchBean searchBean;
@EJB
private LegacyNotificationSenderBean notificationSender;
@Resource
private SessionContext sessionContext;
private FraudRiskSoapClient fraudRiskSoapClient = new FraudRiskSoapClient();
private PolicyCoverageSoapClient policyCoverageSoapClient = new PolicyCoverageSoapClient();
private CustomerMasterDataSoapClient customerMasterDataSoapClient = new CustomerMasterDataSoapClient();
private DocumentVerificationSoapClient documentVerificationSoapClient = new DocumentVerificationSoapClient();
private PaymentReleaseSoapClient paymentReleaseSoapClient = new PaymentReleaseSoapClient();
private LegacyDocumentShareClient documentShareClient = new LegacyDocumentShareClient();
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public LegacyClaimDecisionResponse processClaimDecision(LegacyClaimDecisionRequest request) {
long started = System.currentTimeMillis();
List<String> technicalWarnings = new ArrayList<>();
List<String> businessWarnings = new ArrayList<>();
if (request == null) {
throw new IllegalArgumentException("request must not be null");
}
if (request.getClaimId() == null) {
throw new IllegalArgumentException("claimId must not be null");
}
if (request.getDecisionCode() == null || request.getDecisionCode().trim().isEmpty()) {
throw new IllegalArgumentException("decisionCode must not be empty");
}
if (request.getActorUser() == null || request.getActorUser().trim().isEmpty()) {
request.setActorUser(sessionContext.getCallerPrincipal().getName());
}
if (request.getCorrelationId() == null || request.getCorrelationId().trim().isEmpty()) {
request.setCorrelationId("LEGACY-" + System.currentTimeMillis() + "-" + request.getClaimId());
}
auditBean.writeTechnicalAudit(
request.getClaimId(),
"PROCESS_DECISION_STARTED",
request.getActorUser(),
"Started decision " + request.getDecisionCode() + " with correlation " + request.getCorrelationId()
);
LegacyClaimRecord claim = claimDao.findByIdForUpdate(request.getClaimId());
if (claim == null) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"PROCESS_DECISION_FAILED",
request.getActorUser(),
"Claim not found"
);
throw new IllegalStateException("Claim not found: " + request.getClaimId());
}
String oldStatus = claim.getStatus();
String amountCategory;
if (claim.getClaimAmount() == null) {
amountCategory = "UNKNOWN";
} else if (claim.getClaimAmount().compareTo(new BigDecimal("500")) <= 0) {
amountCategory = "LOW";
} else if (claim.getClaimAmount().compareTo(new BigDecimal("5000")) <= 0) {
amountCategory = "MEDIUM";
} else {
amountCategory = "HIGH";
}
if (!roleChecker.canProcessDecision(oldStatus, request.getDecisionCode(), amountCategory)) {
auditBean.writeSecurityAudit(
request.getClaimId(),
"DECISION_FORBIDDEN",
request.getActorUser(),
"roles=" + roleChecker.currentRoles() + ", status=" + oldStatus + ", decision=" + request.getDecisionCode()
);
throw new SecurityException("User is not allowed to process decision");
}
if ("CLOSED".equals(oldStatus) || "ARCHIVED".equals(oldStatus)) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"DECISION_REJECTED_INVALID_STATUS",
request.getActorUser(),
"Cannot process decision for status " + oldStatus
);
throw new IllegalStateException("Cannot process already closed or archived claim");
}
if ("APPROVE".equals(request.getDecisionCode()) && "NEW".equals(oldStatus)) {
businessWarnings.add("Approving a NEW claim is unusual and may require manual review");
if (!request.isManualOverride()) {
throw new IllegalStateException("NEW claim cannot be approved without manual override");
}
}
LegacyCustomerResponse customerResponse;
try {
customerResponse = customerMasterDataSoapClient.getCustomer(claim.getCustomerId());
if (customerResponse == null || customerResponse.getCustomerId() == null) {
auditBean.writeBusinessAudit(request.getClaimId(), "CUSTOMER_NOT_FOUND", request.getActorUser(), claim.getCustomerId());
throw new IllegalStateException("Customer not found in master data");
}
if ("DECEASED".equals(customerResponse.getCustomerStatus())) {
businessWarnings.add("Customer is marked as deceased in master data");
}
if (customerResponse.getAddressQuality() != null && customerResponse.getAddressQuality().compareTo(new BigDecimal("0.5")) < 0) {
businessWarnings.add("Customer address quality is low");
}
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"CUSTOMER_SERVICE_ERROR",
request.getActorUser(),
ex.getClass().getName() + ": " + ex.getMessage()
);
throw new IllegalStateException("Customer master data service failed", ex);
}
LegacyCoverageResult coverageResult;
try {
coverageResult = policyCoverageSoapClient.calculateCoverage(
claim.getPolicyNo(),
request.getClaimType() == null ? "UNKNOWN" : request.getClaimType(),
claim.getClaimAmount()
);
if (coverageResult == null) {
throw new IllegalStateException("Coverage service returned null");
}
if (!coverageResult.isCovered()) {
if ("APPROVE".equals(request.getDecisionCode())) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"APPROVAL_BLOCKED_NO_COVERAGE",
request.getActorUser(),
"policyNo=" + claim.getPolicyNo()
);
throw new IllegalStateException("Cannot approve claim without policy coverage");
}
businessWarnings.add("Claim is not covered by policy");
}
if (coverageResult.getDeductibleAmount() != null && coverageResult.getDeductibleAmount().compareTo(BigDecimal.ZERO) > 0) {
businessWarnings.add("Deductible amount applies: " + coverageResult.getDeductibleAmount());
}
} catch (RuntimeException ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"COVERAGE_SERVICE_ERROR",
request.getActorUser(),
ex.getMessage()
);
throw ex;
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"COVERAGE_SERVICE_ERROR",
request.getActorUser(),
ex.getMessage()
);
throw new IllegalStateException("Coverage service failed", ex);
}
FraudRiskLegacyResponse fraudResponse;
try {
fraudResponse = fraudRiskSoapClient.checkRisk(
claim.getClaimId(),
claim.getCustomerId(),
claim.getPolicyNo(),
claim.getClaimAmount()
);
BigDecimal riskScore = new BigDecimal(fraudResponse.getScore());
claimDao.updateRiskScore(claim.getClaimId(), riskScore);
if (riskScore.compareTo(new BigDecimal("800")) >= 0) {
if ("APPROVE".equals(request.getDecisionCode()) && !request.isManualOverride()) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"APPROVAL_BLOCKED_HIGH_RISK",
request.getActorUser(),
"riskScore=" + riskScore
);
throw new IllegalStateException("High fraud risk requires manual override");
}
businessWarnings.add("High fraud risk score: " + riskScore);
}
if (fraudResponse.isManualReviewRequired() && !request.isManualOverride()) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"MANUAL_REVIEW_REQUIRED",
request.getActorUser(),
fraudResponse.getRiskCategory()
);
if ("APPROVE".equals(request.getDecisionCode())) {
throw new IllegalStateException("Manual fraud review required before approval");
}
}
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"FRAUD_SERVICE_ERROR",
request.getActorUser(),
ex.getClass().getName() + ": " + ex.getMessage()
);
if ("APPROVE".equals(request.getDecisionCode())) {
throw new IllegalStateException("Cannot approve because fraud service failed", ex);
}
technicalWarnings.add("Fraud service failed: " + ex.getMessage());
fraudResponse = new FraudRiskLegacyResponse();
fraudResponse.setScore("999");
fraudResponse.setRiskCategory("UNKNOWN_TECHNICAL_ERROR");
fraudResponse.setManualReviewRequired(true);
}
if ("APPROVE".equals(request.getDecisionCode()) || "REJECT".equals(request.getDecisionCode())) {
List<LegacyDocumentMetadata> documents = documentBean.findDocumentsForClaim(request.getClaimId());
if (documents == null || documents.isEmpty()) {
if ("APPROVE".equals(request.getDecisionCode())) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"APPROVAL_BLOCKED_NO_DOCUMENTS",
request.getActorUser(),
"No documents found"
);
throw new IllegalStateException("Cannot approve without documents");
}
businessWarnings.add("No documents found");
} else {
boolean allDocumentsReadable = true;
boolean allDocumentsVerified = true;
for (LegacyDocumentMetadata doc : documents) {
if (doc.getAbsolutePath() == null || !documentShareClient.exists(doc.getAbsolutePath())) {
allDocumentsReadable = false;
auditBean.writeTechnicalAudit(
request.getClaimId(),
"DOCUMENT_FILE_MISSING",
request.getActorUser(),
"documentId=" + doc.getDocumentId() + ", path=" + doc.getAbsolutePath()
);
} else {
try {
LegacyDocumentVerificationResult result = documentVerificationSoapClient.verify(
doc.getDocumentId(),
doc.getAbsolutePath(),
doc.getDocumentType()
);
if (result == null || !result.isVerified()) {
allDocumentsVerified = false;
auditBean.writeBusinessAudit(
request.getClaimId(),
"DOCUMENT_NOT_VERIFIED",
request.getActorUser(),
"documentId=" + doc.getDocumentId()
);
}
} catch (Exception ex) {
technicalWarnings.add("Document verification failed for " + doc.getDocumentId());
allDocumentsVerified = false;
auditBean.writeTechnicalAudit(
request.getClaimId(),
"DOCUMENT_VERIFICATION_ERROR",
request.getActorUser(),
ex.getMessage()
);
}
}
}
if (!allDocumentsReadable && "APPROVE".equals(request.getDecisionCode())) {
throw new IllegalStateException("Cannot approve because some document files are missing");
}
if (!allDocumentsVerified && "APPROVE".equals(request.getDecisionCode()) && !request.isManualOverride()) {
throw new IllegalStateException("Cannot approve because some documents are not verified");
}
claimDao.markDocumentStatus(request.getClaimId(), allDocumentsVerified ? "VERIFIED" : "MANUAL_REVIEW");
}
}
String effectiveDecision = request.getDecisionCode();
if ("APPROVE".equals(effectiveDecision)) {
if (coverageResult.getApprovedAmount() != null && coverageResult.getApprovedAmount().compareTo(BigDecimal.ZERO) <= 0) {
auditBean.writeBusinessAudit(
request.getClaimId(),
"APPROVAL_BLOCKED_ZERO_AMOUNT",
request.getActorUser(),
"approvedAmount=" + coverageResult.getApprovedAmount()
);
throw new IllegalStateException("Approved amount must be positive");
}
if ("HIGH".equals(amountCategory) && !roleChecker.currentRoles().contains("CLAIMS_MANAGER")) {
throw new SecurityException("High amount approval requires CLAIMS_MANAGER");
}
} else if ("REJECT".equals(effectiveDecision)) {
if (request.getReason() == null || request.getReason().length() < 20) {
throw new IllegalArgumentException("Reject reason must contain at least 20 characters");
}
} else if ("ESCALATE".equals(effectiveDecision)) {
if (request.getReason() == null || request.getReason().trim().isEmpty()) {
request.setReason("Escalated from backoffice without detailed reason");
}
} else if ("REQUEST_MORE_INFO".equals(effectiveDecision)) {
if (request.getPartnerMessage() == null || request.getPartnerMessage().trim().isEmpty()) {
throw new IllegalArgumentException("Partner message required for more info request");
}
} else {
throw new IllegalArgumentException("Unknown decision code: " + effectiveDecision);
}
StoredProcedureDecisionResult storedProcedureResult = claimDao.callProcessClaimDecision(
request.getClaimId(),
effectiveDecision,
request.getActorUser(),
request.getReason()
);
String newStatus = storedProcedureResult.getNewStatus();
auditBean.writeBusinessAudit(
request.getClaimId(),
"DECISION_PROCESSED",
request.getActorUser(),
"oldStatus=" + oldStatus + ", newStatus=" + newStatus + ", decision=" + effectiveDecision
);
if ("APPROVE".equals(effectiveDecision)) {
try {
BigDecimal approvedAmount = coverageResult.getApprovedAmount() == null
? claim.getClaimAmount()
: coverageResult.getApprovedAmount();
if (coverageResult.getDeductibleAmount() != null) {
approvedAmount = approvedAmount.subtract(coverageResult.getDeductibleAmount());
}
if (approvedAmount.compareTo(BigDecimal.ZERO) < 0) {
approvedAmount = BigDecimal.ZERO;
}
if (request.isForcePayment() || approvedAmount.compareTo(BigDecimal.ZERO) > 0) {
LegacyPaymentReleaseResponse paymentResponse = paymentReleaseSoapClient.releasePayment(
request.getClaimId(),
claim.getCustomerId(),
approvedAmount,
request.getActorUser()
);
if (paymentResponse == null || !"OK".equals(paymentResponse.getStatus())) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"PAYMENT_RELEASE_FAILED",
request.getActorUser(),
paymentResponse == null ? "null response" : paymentResponse.getErrorMessage()
);
throw new IllegalStateException("Payment release failed");
}
paymentBean.markPaymentReleased(request.getClaimId(), paymentResponse.getPaymentReference(), approvedAmount);
}
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"PAYMENT_ERROR",
request.getActorUser(),
ex.getMessage()
);
throw new IllegalStateException("Claim approved but payment failed", ex);
}
}
try {
eventPublisher.publishDecisionChanged(
request.getClaimId(),
oldStatus,
newStatus,
request.getActorUser(),
request.getCorrelationId()
);
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"EVENT_PUBLISH_FAILED",
request.getActorUser(),
ex.getMessage()
);
throw ex;
}
try {
if ("APPROVE".equals(effectiveDecision)) {
notificationSender.sendClaimApprovedNotification(request.getClaimId(), claim.getCustomerId(), claim.getPartnerId());
} else if ("REJECT".equals(effectiveDecision)) {
notificationSender.sendClaimRejectedNotification(request.getClaimId(), claim.getCustomerId(), claim.getPartnerId(), request.getReason());
} else if ("ESCALATE".equals(effectiveDecision)) {
notificationSender.sendEscalationNotification(request.getClaimId(), request.getActorUser(), request.getReason());
} else if ("REQUEST_MORE_INFO".equals(effectiveDecision)) {
notificationSender.sendMoreInfoRequest(request.getClaimId(), claim.getPartnerId(), request.getPartnerMessage());
}
} catch (Exception ex) {
technicalWarnings.add("Notification failed: " + ex.getMessage());
auditBean.writeTechnicalAudit(
request.getClaimId(),
"NOTIFICATION_FAILED",
request.getActorUser(),
ex.getMessage()
);
}
try {
searchBean.refreshClaimSearchCache(request.getClaimId());
} catch (Exception ex) {
technicalWarnings.add("Search refresh failed: " + ex.getMessage());
auditBean.writeTechnicalAudit(
request.getClaimId(),
"SEARCH_REFRESH_FAILED",
request.getActorUser(),
ex.getMessage()
);
}
if ("ESCALATE".equals(effectiveDecision)) {
try {
claimDao.createEscalationRecord(
request.getClaimId(),
request.getActorUser(),
request.getReason(),
Instant.now().plusSeconds(86400).toString()
);
} catch (Exception ex) {
auditBean.writeTechnicalAudit(
request.getClaimId(),
"ESCALATION_RECORD_FAILED",
request.getActorUser(),
ex.getMessage()
);
throw new IllegalStateException("Could not create escalation record", ex);
}
}
long duration = System.currentTimeMillis() - started;
auditBean.writeTechnicalAudit(
request.getClaimId(),
"PROCESS_DECISION_FINISHED",
request.getActorUser(),
"durationMs=" + duration + ", technicalWarnings=" + technicalWarnings + ", businessWarnings=" + businessWarnings
);
LegacyClaimDecisionResponse response = new LegacyClaimDecisionResponse();
response.setClaimId(request.getClaimId());
response.setOldStatus(oldStatus);
response.setNewStatus(newStatus);
response.setDecisionCode(effectiveDecision);
response.setAuditId(storedProcedureResult.getAuditId());
response.setCorrelationId(request.getCorrelationId());
response.setTechnicalWarnings(technicalWarnings);
response.setBusinessWarnings(businessWarnings);
response.setMessage("Decision processed with status " + newStatus);
return response;
}
}Warum diese Methode schlecht ist
Diese Methode verletzt fast jedes wichtige Refactoring-Prinzip:
| Problem | Beispiel | Folge |
|---|---|---|
| Zu viele Verantwortlichkeiten | Security, Dokumente, Payment, Audit, JMS, Search | schwer testbar |
| Fachlogik in EJB | Statusregeln direkt in Bean | WebSphere-Abhängigkeit |
| Externe Systeme direkt | SOAP-Clients im Ablauf | keine Ports/Adapter |
| Transaktion zu groß | JTA über DB, SOAP, JMS | Seiteneffekte schwer kontrollierbar |
| Fehlerlogik chaotisch | IllegalStateException für Fachfälle |
keine klare Fehlersemantik |
| Statuslogik verteilt | Java + Stored Procedure | schwer nachvollziehbar |
| Rollen hart kodiert | CLAIMS_MANAGER im Code |
Security-Migration schwer |
| Dokumente hart gekoppelt | File Share direkt | Object Storage schwer |
| Payment synchron | Freigabe im Entscheidungsablauf | keine Idempotenz |
| Event ohne Outbox | JMS direkt | Inkonsistenz möglich |
| Tests schwierig | EJB/JNDI/SOAP/JMS nötig | kaum Unit Tests möglich |
Erste Refactoring-Kandidaten aus der Monster Method
| Bereich | Späterer Zielbaustein | Pattern / Konzept |
|---|---|---|
| Request-Validierung | ProcessClaimDecisionCommand |
Command Pattern |
| Rollenprüfung | AuthorizationPolicy |
Policy Object |
| Statuswechsel | ClaimState, ClaimTransitionPolicy |
State Pattern |
| Coverage | CoveragePort |
Port & Adapter |
| Fraud | FraudRiskPort |
Anti-Corruption Layer |
| Dokumentprüfung | DocumentVerificationPort |
Port & Adapter |
| Payment | PaymentReleasePort |
Outbox, Idempotency |
| Audit | AuditLogPort |
Audit Boundary |
| Notification | NotificationPort |
Outbox / Adapter |
| Suche | ClaimSearchPort |
Read Model / Adapter |
| SLA/Eskalation | EscalationPolicy |
Batch Worker |
| Orchestrierung | ClaimDecisionWorkflowOrchestrator |
Workflow Orchestrator |
Legacy SLA & Escalation Scheduler
Customer Support und SLA-Eskalation
Support-Triage, fachliche Bearbeitung und SLA-Eskalation benötigen klare Übergaben und nachvollziehbare Verantwortungen.
- Die Triage klassifiziert und weist zu.
- Fachteams lösen Claims-, Zahlungs- oder Dokumentprobleme.
- Eine Eskalation erzeugt Sichtbarkeit und Priorität, aber keine automatische Genehmigung.
SlaEscalationTimerBean.java
package com.seb4u.demo.claims.legacy.scheduler;
import com.seb4u.demo.claims.legacy.dao.LegacyClaimDao;
import com.seb4u.demo.claims.legacy.ejb.LegacyNotificationSenderBean;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimerService;
import java.util.List;
@Singleton
@Startup
public class SlaEscalationTimerBean {
@Resource
private TimerService timerService;
@EJB
private LegacyClaimDao claimDao;
@EJB
private LegacyNotificationSenderBean notificationSender;
@PostConstruct
public void init() {
// In production, this bean sometimes starts twice during deployment restart.
}
@Schedule(hour = "*/1", minute = "0", persistent = false)
public void checkOpenClaimsAndEscalate() {
List<Long> overdueClaims = claimDao.findClaimsWithBreachedSla();
for (Long claimId : overdueClaims) {
try {
claimDao.markClaimEscalatedByScheduler(claimId, "SLA_BREACHED");
claimDao.insertSchedulerAudit(claimId, "SLA escalation created by WebSphere TimerBean");
notificationSender.sendEscalationNotification(claimId, "SYSTEM", "SLA breached");
} catch (Exception ex) {
// Legacy: log only, continue. No retry model, no dead letter, no lease lock.
System.err.println("Could not escalate claim " + claimId + ": " + ex.getMessage());
}
}
}
}Legacy-Probleme:
- Kein Lease Lock.
- Bei mehreren Knoten können Jobs doppelt laufen.
- Keine klare Batch-Domäne.
- Keine idempotente Eskalationsregel.
- Kein OpenShift CronJob-Modell.
Legacy Search Indexer
LegacyClaimSearchDao.java
package com.seb4u.demo.claims.legacy.dao;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
@Stateless
public class LegacyClaimSearchDao {
@Resource(lookup = "jdbc/ClaimsOracleDS")
private DataSource dataSource;
public List<LegacyClaimSearchRow> search(String freeText, String status, String partnerId) {
String sql = "select CLAIM_ID, CUSTOMER_ID, POLICY_NO, STATUS, PARTNER_ID, CLAIM_AMOUNT " +
"from CLAIM " +
"where (upper(CUSTOMER_ID) like upper(?) " +
" or upper(POLICY_NO) like upper(?) " +
" or upper(PARTNER_ID) like upper(?)) ";
if (status != null && !status.trim().isEmpty()) {
sql += " and STATUS = '" + status + "' ";
}
if (partnerId != null && !partnerId.trim().isEmpty()) {
sql += " and PARTNER_ID = '" + partnerId + "' ";
}
sql += " order by LAST_UPDATED_AT desc";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
String pattern = "%" + freeText + "%";
ps.setString(1, pattern);
ps.setString(2, pattern);
ps.setString(3, pattern);
try (ResultSet rs = ps.executeQuery()) {
List<LegacyClaimSearchRow> rows = new ArrayList<>();
while (rs.next()) {
rows.add(new LegacyClaimSearchRow(
rs.getLong("CLAIM_ID"),
rs.getString("CUSTOMER_ID"),
rs.getString("POLICY_NO"),
rs.getString("STATUS"),
rs.getString("PARTNER_ID"),
rs.getBigDecimal("CLAIM_AMOUNT")
));
}
return rows;
}
} catch (Exception e) {
throw new IllegalStateException("Search failed", e);
}
}
}Legacy-Probleme:
- SQL-Injection-Risiko über
statusundpartnerId. - Oracle LIKE ist keine echte Suchdomäne.
- Kein Search Read Model.
- Keine Trennung zwischen Backoffice-Suche und Partner-Suche.
Legacy Audit & Compliance Reporter
AuditReportStoredProcedureDao.java
package com.seb4u.demo.claims.legacy.dao;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import java.io.FileWriter;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
@Stateless
public class AuditReportStoredProcedureDao {
@Resource(lookup = "jdbc/ClaimsOracleDS")
private DataSource dataSource;
public String exportMonthlyAuditCsv(String month) {
String path = "//legacy-fileserver01/claims-reports/audit/audit_" + month + ".csv";
try (Connection connection = dataSource.getConnection();
CallableStatement statement = connection.prepareCall("{call SP_EXPORT_AUDIT_REPORT(?)}");
FileWriter writer = new FileWriter(path)) {
statement.setString(1, month);
boolean hasResultSet = statement.execute();
if (hasResultSet) {
try (ResultSet rs = statement.getResultSet()) {
writer.write("claimId;action;actor;createdAt;description\n");
while (rs.next()) {
writer.write(rs.getLong("CLAIM_ID") + ";" +
rs.getString("ACTION_CODE") + ";" +
rs.getString("ACTOR_USER") + ";" +
rs.getTimestamp("CREATED_AT") + ";" +
rs.getString("DESCRIPTION") + "\n");
}
}
}
return path;
} catch (Exception e) {
throw new IllegalStateException("Could not export audit report", e);
}
}
}Legacy-Probleme:
- Compliance Export schreibt auf File Share.
- Kein fachliches Audit Trail Model.
- CSV-Format ist nicht versioniert.
- Technische Logs und fachliche Audit-Ereignisse werden vermischt.
Legacy Notification System
LegacyNotificationSenderBean.java
package com.seb4u.demo.claims.legacy.ejb;
import javax.ejb.Stateless;
import java.nio.file.Files;
import java.nio.file.Path;
@Stateless
public class LegacyNotificationSenderBean {
private static final String TEMPLATE_ROOT = "//legacy-fileserver01/claims-notification-templates";
public void sendClaimApprovedNotification(Long claimId, String customerId, String partnerId) {
String template = readTemplate("claim_approved.txt");
String text = template
.replace("${claimId}", String.valueOf(claimId))
.replace("${customerId}", customerId)
.replace("${partnerId}", partnerId);
sendMail(customerId, "Schaden genehmigt", text);
sendPartnerMessage(partnerId, text);
}
public void sendClaimRejectedNotification(Long claimId, String customerId, String partnerId, String reason) {
String template = readTemplate("claim_rejected.txt");
String text = template
.replace("${claimId}", String.valueOf(claimId))
.replace("${reason}", reason);
sendMail(customerId, "Schaden abgelehnt", text);
sendPartnerMessage(partnerId, text);
}
public void sendEscalationNotification(Long claimId, String actor, String reason) {
String template = readTemplate("claim_escalated.txt");
String text = template
.replace("${claimId}", String.valueOf(claimId))
.replace("${actor}", actor)
.replace("${reason}", reason);
sendMail("claims-teamlead@legacy.local", "Eskalation", text);
}
public void sendMoreInfoRequest(Long claimId, String partnerId, String message) {
sendPartnerMessage(partnerId, "Bitte weitere Informationen zu Schaden " + claimId + ": " + message);
}
private String readTemplate(String file) {
try {
return Files.readString(Path.of(TEMPLATE_ROOT, file));
} catch (Exception e) {
throw new IllegalStateException("Could not read notification template " + file, e);
}
}
private void sendMail(String receiver, String subject, String text) {
// Legacy SMTP gateway call hidden here
}
private void sendPartnerMessage(String partnerId, String text) {
// Legacy partner message table insert hidden here
}
}Legacy-Probleme:
- Templates liegen auf File Share.
- Keine Template Policy.
- Kein Outbox Event.
- Keine Retry-/Dead-Letter-Strategie.
- Kein sauberer Notification Port.
Legacy-Dateien für späteres Before/After-Mapping
Diese Runde legt bereits konkrete Legacy-Dateien fest, die später in Runde 7 und im finalen Mapping verwendet werden.
| Legacy-Datei / Modul | Hauptproblem | Späteres Ziel |
|---|---|---|
LegacyClaimFacadeBean.java |
Monster Method, EJB-Zentralismus | ProcessClaimDecisionHandler,
ClaimDecisionWorkflowOrchestrator |
LegacyClaimStatusIfElse.java |
Statuslogik in if/else | ClaimState, ClaimTransitionPolicy |
LegacyDocumentShareClient.java |
File Share hart verdrahtet | DocumentStoragePort,
FileShareDocumentAdapter |
FraudRiskSoapClient.java |
direkter SOAP-Aufruf | FraudRiskPort, FraudRiskSoapAdapter |
PolicyCoverageSoapClient.java |
Coverage-Logik im Client | CoveragePort, CoverageSoapAdapter,
CoveragePolicy |
PaymentReleaseSoapClient.java |
synchrone Zahlung ohne Idempotenz | PaymentReleasePort,
PaymentReleaseSoapAdapter, Outbox |
LegacyNotificationSenderBean.java |
direkte Benachrichtigung | NotificationPort, NotificationAdapter |
SlaEscalationTimerBean.java |
TimerBean ohne Lease Lock | EscalationBatchJob, EscalationPolicy |
AuditReportStoredProcedureDao.java |
Reporting über Stored Procedure | AuditLogPort, AuditReportingAdapter |
LegacyClaimSearchDao.java |
Oracle LIKE Suche | ClaimSearchPort, SearchIndexAdapter |
CustomerMasterDataSoapClient.java |
direkte Kundendatenkopplung | CustomerMasterDataPort,
CustomerMasterDataSoapAdapter |
JaasRoleChecker.java |
WebSphere/JAAS-Rollen im Code | SecurityContextPort,
AuthorizationPolicy |
LegacyPartnerPortalServlet.java |
JSP/Servlet an EJB gekoppelt | PartnerPortalRestController,
PartnerPortalBffAdapter |
InternalClaimsBackofficeBean.java |
Backoffice an SOAP-DTO gekoppelt | ClaimBackofficeUseCase |
Testprobleme im Legacy-Ausgangssystem
Warum kaum Unit Tests existieren
- Klassen benötigen EJB Container.
- JNDI Lookup ist überall.
- SOAP Clients sind nicht abstrahiert.
- File Shares werden direkt verwendet.
- DB-Transaktionen sind schwer isolierbar.
- Stored Procedures enthalten Fachlogik.
- Security hängt an
SessionContext. - JMS wird direkt publiziert.
- Es gibt keine Ports/Fakes.
Erste Characterization-Test-Idee für Runde 3
Spätere Tests sollen nicht sofort „schönen Code“ erzwingen. Zuerst müssen sie Legacy-Verhalten dokumentieren.
Beispielhafte Testfälle:
| Testfall | Erwartung |
|---|---|
| APPROVE mit hoher Fraud Risk ohne Override | fachliche Ablehnung |
| APPROVE ohne Dokumente | fachliche Ablehnung |
| REJECT mit kurzer Begründung | Validierungsfehler |
| ESCALATE ohne Grund | Default-Grund wird gesetzt |
| Coverage nicht gedeckt + APPROVE | fachliche Ablehnung |
| Payment Service Fehler nach Approval | Transaktions-/Seiteneffektproblem sichtbar machen |
| JMS Fehler nach Statusupdate | Inkonsistenz sichtbar machen |
| Fraud Service Timeout | Legacy-Fallback dokumentieren |
Risiken, die vor Refactoring verstanden werden müssen
| Risiko | Warum kritisch? | Sicherheitsnetz in späterer Runde |
|---|---|---|
| Statuslogik verteilt | Java und PL/SQL ändern Status | Golden Master + State Transition Tests |
| Payment-Seiteneffekte | Zahlung kann ausgelöst sein, obwohl DB rollbackt | Idempotency + Compensation Tests |
| JMS ohne Outbox | Event kann fehlen oder doppelt sein | Outbox Tests |
| Dokumente als File Paths | Migration zu Object Storage schwierig | Adapter Tests |
| Security in JAAS | OpenShift/OIDC Migration schwierig | AuthorizationPolicy Tests |
| Stored Procedures | Fachlogik schwer sichtbar | Characterization Tests gegen Test-DB/Fake |
| Partner Portal Kopplung | UI-Strangler schwierig | Contract Tests |
| Backoffice Kopplung | Sachbearbeiter-Workflow schwer ablösbar | Use Case Tests |
Runde-2-Ergebnis
Diese Runde hat das Legacy-Ausgangssystem konkretisiert:
- EAR/WAR/EJB-Struktur
- WebSphere-Deskriptoren
- JPA/JDBC/Oracle/Stored-Procedure-Mischung
- SOAP-Endpunkt
- SOAP-Clients zu Zusatzsystemen
- JMS Publisher über IBM MQ
- LDAP/JAAS-Rollenprüfung
- File-Share-Dokumentenlogik
- Partner Portal Servlet
- Internal Backoffice Bean
- SLA TimerBean
- Search DAO
- Audit/Compliance Reporter
- Notification Sender
- zentrale Monster Method
processClaimDecision(...)
Das ist jetzt die Grundlage für Runde 3: Refactoring-Lerninhalt Schritt für Schritt.
Erzeugte Kapitel / Module dieser Runde
- Legacy-Gesamtbild vor Refactoring
- Legacy-EAR-Struktur
- WebSphere-Deskriptoren
- Legacy-Datenbankmodell
- Stored Procedures
- SOAP-Operationen
- JMS/IBM MQ
- LDAP/JAAS Security
- Document Management Adapter
- Legacy DAO
- Partner Portal
- Internal Claims Backoffice
- Zusatzsystem-Adapter
- Monster Method
processClaimDecision(...) - SLA & Escalation Scheduler
- Search Indexer
- Audit & Compliance Reporter
- Notification System
- Before/After-Mapping-Vorbereitung
- Testprobleme
- Risiken vor Refactoring
Wichtige Codebeispiele dieser Runde
application.xmlibm-application-bnd.xmlejb-jar.xmlpersistence.xmlSP_PROCESS_CLAIM_DECISIONSP_REFRESH_CLAIM_REPORTINGLegacyClaimSoapEndpoint.javaLegacyClaimEventPublisher.javaJaasRoleChecker.javaLegacyDocumentShareClient.javaLegacyClaimDao.javaLegacyPartnerPortalServlet.javaInternalClaimsBackofficeBean.javaFraudRiskSoapClient.javaPolicyCoverageSoapClient.javaPaymentReleaseSoapClient.javaLegacyClaimFacadeBean.processClaimDecision(...)SlaEscalationTimerBean.javaLegacyClaimSearchDao.javaAuditReportStoredProcedureDao.javaLegacyNotificationSenderBean.java
044. Refactoring Schritt für Schritt
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Refactoring Schritt für Schritt in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Projekt: Legacy Claims & Customer Support Enterprise System
Runde: 03
Thema: Refactoring der Legacy-Claims-Monster-Method und der gekoppelten Systemlandschaft Schritt für Schritt
Datum / Reihenfolge: 2026-07-05 / Runde 03
Ziel dieser Runde
Diese Runde konzentriert sich auf den wichtigsten Lernteil des Projekts: Wie wird ein altes, eng gekoppeltes Java-Enterprise-System so refactored, dass es verständlich, testbar, modularisierbar und später nach OpenShift migrierbar wird?
Im Mittelpunkt steht die Legacy-Monster-Method:
processClaimDecision(...)Aus Runde 2 kennen wir die typischen Probleme:
- Fachlogik, Workflow, SOAP, JMS, Stored Procedures, LDAP/JAAS, Dokumentenprüfung, Zahlungsfreigabe, Audit und Notification sind in einer EJB-Methode vermischt.
- Statusübergänge sind als if/else-Ketten implementiert.
- Technische Exceptions werden als fachlicher Entscheidungsfluss verwendet.
- Externe Systeme werden direkt aus der Fachlogik aufgerufen.
- JTA-Transaktion, Remote Calls und File-System-Zugriffe sind in einem Ablauf vermischt.
- Tests fehlen fast vollständig.
- Jede Änderung ist riskant, weil Nebenwirkungen nicht sichtbar sind.
Diese Runde zeigt deshalb nicht nur das Zielbild, sondern den Weg dorthin.
Refactoring-Grundregel für dieses Projekt
Bei einem alten Enterprise-System wird nicht zuerst modernisiert. Zuerst wird stabilisiert.
Die Reihenfolge lautet:
- Verhalten sichtbar machen.
- Sicherheitsnetz mit Tests aufbauen.
- Refactoring-Seams schaffen.
- Fachliche Begriffe herausarbeiten.
- technische Abhängigkeiten hinter Ports verstecken.
- Status- und Workflowlogik aus der EJB herausziehen.
- Integrationen über Adapter kapseln.
- Transaktionen und Events kontrolliert neu schneiden.
- erst danach Richtung OpenShift, REST, BFF, moderne UI und modulare Maven-Struktur gehen.
Merksatz:
Migration ohne Refactoring verschiebt nur Chaos in Container. Refactoring ohne Sicherheitsnetz erzeugt neues Chaos. Deshalb: erst verstehen, dann testen, dann schneiden, dann migrieren.
Refactoring-Roadmap dieser Runde
Schrittweises Strangler-Refactoring
Die Modernisierung bleibt kontrollierbar, wenn jeder Schnitt durch Tests abgesichert und technisch reversibel ist.
- Zuerst Verhalten erfassen, danach Strukturen verändern.
- Seams und Ports reduzieren Kopplung, ohne sofort Systeme abzuschalten.
- Alte Pfade werden erst nach messbarer Gleichwertigkeit entfernt.
Die Refactoring-Schritte dieser Runde sind:
| Schritt | Titel | Hauptziel | Pattern / Konzept |
|---|---|---|---|
| 01 | Inventory und Characterization | Verhalten erfassen | Characterization Test, Golden Master |
| 02 | Refactoring Seam einführen | Methode aufrufbar und messbar machen | Sprout Method, Facade Stabilisierung |
| 03 | Command Object extrahieren | Parameterchaos reduzieren | Command Pattern, DTO |
| 04 | Ergebnisobjekt einführen | technische Exceptions reduzieren | Result Object |
| 05 | Statuslogik isolieren | if/else-Ketten herausziehen | State Pattern, Policy Object |
| 06 | Dokumentenlogik extrahieren | File Share entkoppeln | Port, Adapter, Anti-Corruption Layer |
| 07 | Coverage/Fraud/Payment als Ports | externe SOAP-Kopplung lösen | Ports & Adapters |
| 08 | Customer/Identity Ports | Stammdaten und Rollen abstrahieren | Security Context Port, ACL |
| 09 | Workflow Orchestrator | Ablauf steuerbar machen | Workflow Orchestrator, Application Service |
| 10 | Audit Trail abstrahieren | versteckte Audit-Logik sichtbar machen | Audit Port, Domain Event |
| 11 | Notification/Search/Reporting trennen | Nebenwirkungen entfernen | Outbox, Read Model |
| 12 | Idempotency und Outbox | Wiederholbarkeit schaffen | Idempotency Key, Outbox Pattern |
| 13 | Batch/SLA trennen | Scheduler aus Fachlogik lösen | Batch Worker, Lease Lock |
| 14 | Legacy UI entkoppeln | Partner Portal und Backoffice ablösen | Strangler UI, REST/BFF Boundary |
Ausgangspunkt: problematische Legacy-Verantwortungen
Die alte LegacyClaimFacadeBean ist nicht nur eine
Fassade. Sie ist gleichzeitig:
- SOAP Service Backend
- Application Service
- Domain Service
- Workflow Engine
- Security Service
- Audit Writer
- JMS Publisher
- Stored-Procedure-Orchestrator
- Dokumentenmanager
- Payment Gateway
- Notification Gateway
- SLA-Regelprüfer
- technische Fehlerbehandlung
Das ist ein klassisches Enterprise-Anti-Pattern:
Eine technische Fassade wird mit der Zeit zum fachlichen Zentralhirn.
Problematische Verantwortungskarte
| Verantwortung | Legacy-Ort | Problem | Ziel-Ort nach Refactoring |
|---|---|---|---|
| Claim-Entscheidung | LegacyClaimFacadeBean |
Monster Method | ProcessClaimDecisionHandler |
| Statusübergänge | if/else in EJB | untestbar | ClaimTransitionPolicy |
| Dokumentenprüfung | direkter File/SOAP-Aufruf | harte Infrastrukturkopplung | DocumentVerificationPort |
| Coverage | SOAP Client direkt | technische Kopplung | CoveragePort |
| Fraud | SOAP Client direkt | Timeouts als Fachfluss | FraudRiskPort |
| Payment | SOAP/JMS direkt | Transaktionsmischung | PaymentReleasePort + Outbox |
| Audit | Stored Procedure | versteckt | AuditLogPort |
| Notification | JMS/SOAP direkt | Nebenwirkung in Transaktion | NotificationPort + Outbox |
| Rollenprüfung | JAAS direkt | hart kodiert | SecurityContextPort +
AuthorizationPolicy |
| Search | Oracle LIKE | schlechte Performance | ClaimSearchPort / Read Model |
Teil A: Sicherheitsnetz vor Refactoring
Schritt 01: Inventory und Characterization
Was war vorher schlecht?
Vor dem Refactoring weiß niemand exakt, welches Verhalten die Monster Method in allen Sonderfällen hat. Besonders gefährlich sind:
- Altstatus wie
PENDING_REVIEW,WAITING_FOR_DOCUMENTS,ESCALATED,PAYMENT_PENDING - unterschiedliche Rollen wie
CLAIMS_AGENT,CLAIMS_MANAGER,PARTNER_USER - Fehlerfälle der SOAP Services
- fehlende Dokumente
- Betrugsverdacht
- abgelaufene Police
- Stored-Procedure-Nebenwirkungen
- teilweise versendete JMS-Nachrichten
Was wird geändert?
Noch nichts am Produktivcode. Zuerst wird beobachtet.
Wir erstellen:
- ein Inventory der Legacy-Dateien,
- eine Liste fachlicher Szenarien,
- Characterization Tests,
- Golden-Master-Ausgaben,
- Testdaten-Snapshots.
Pattern / Konzept
- Characterization Test
- Golden Master
- Test Data Builder
- Fake Object
- Spy
Tests / Sicherheitsnetz
Tests prüfen zunächst nicht, ob das Legacy-Verhalten schön ist. Sie prüfen, ob das aktuelle Verhalten stabil reproduzierbar ist.
Risiko
Characterization Tests können schlechtes Verhalten konservieren. Deshalb werden sie später schrittweise von fachlich sauberen Tests ersetzt.
Testdatenmodell für Characterization Tests
Datei:
migration-tests/src/test/resources/golden-master/claim-decision-cases.ymlcases:
- id: CLAIM_APPROVE_SIMPLE
claimId: CLM-10001
customerId: CUST-4711
policyNumber: POL-2024-0001
currentStatus: PENDING_REVIEW
requestedDecision: APPROVE
actor: claims.manager
roles:
- CLAIMS_MANAGER
documents:
- type: DAMAGE_PHOTO
verified: true
- type: INVOICE
verified: true
fraudScore: 12
coverage:
covered: true
deductibleAmount: 250.00
maxPayableAmount: 5000.00
expectedLegacyStatus: PAYMENT_PENDING
expectedPaymentPrepared: true
expectedAuditEvents:
- CLAIM_DECISION_STARTED
- COVERAGE_CHECKED
- FRAUD_CHECKED
- DOCUMENTS_VERIFIED
- PAYMENT_PREPARED
- CLAIM_APPROVED
- id: CLAIM_REJECT_FRAUD_HIGH_RISK
claimId: CLM-10002
customerId: CUST-4712
policyNumber: POL-2024-0002
currentStatus: PENDING_REVIEW
requestedDecision: APPROVE
actor: claims.agent
roles:
- CLAIMS_AGENT
documents:
- type: DAMAGE_PHOTO
verified: true
fraudScore: 91
coverage:
covered: true
deductibleAmount: 0.00
maxPayableAmount: 10000.00
expectedLegacyStatus: MANUAL_FRAUD_REVIEW
expectedPaymentPrepared: false
expectedAuditEvents:
- CLAIM_DECISION_STARTED
- FRAUD_CHECKED
- CLAIM_SENT_TO_FRAUD_REVIEW
- id: CLAIM_WAIT_FOR_DOCUMENTS
claimId: CLM-10003
customerId: CUST-4713
policyNumber: POL-2024-0003
currentStatus: WAITING_FOR_DOCUMENTS
requestedDecision: APPROVE
actor: claims.manager
roles:
- CLAIMS_MANAGER
documents:
- type: DAMAGE_PHOTO
verified: false
fraudScore: 20
coverage:
covered: true
deductibleAmount: 100.00
maxPayableAmount: 2000.00
expectedLegacyStatus: WAITING_FOR_DOCUMENTS
expectedPaymentPrepared: false
expectedAuditEvents:
- CLAIM_DECISION_STARTED
- DOCUMENTS_MISSING
- CUSTOMER_NOTIFICATION_REQUESTEDLegacy Characterization Test
Datei:
migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyProcessClaimDecisionCharacterizationTest.javapackage com.seb4u.demo.claims.migration;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.support.FakeLegacyAuditDao;
import com.seb4u.demo.claims.legacy.support.FakeLegacyClaimDao;
import com.seb4u.demo.claims.legacy.support.FakeLegacyDocumentShareClient;
import com.seb4u.demo.claims.legacy.support.FakeLegacyEventPublisher;
import com.seb4u.demo.claims.legacy.support.FakeLegacyPaymentSoapClient;
import com.seb4u.demo.claims.legacy.support.FakePolicyCoverageSoapClient;
import com.seb4u.demo.claims.legacy.support.FakeFraudRiskSoapClient;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LegacyProcessClaimDecisionCharacterizationTest {
@Test
void approveSimpleClaim_keepsCurrentLegacyBehaviourVisible() throws Exception {
FakeLegacyClaimDao claimDao = new FakeLegacyClaimDao();
FakeLegacyAuditDao auditDao = new FakeLegacyAuditDao();
FakeLegacyEventPublisher eventPublisher = new FakeLegacyEventPublisher();
claimDao.givenClaim("CLM-10001")
.withCustomerId("CUST-4711")
.withPolicyNumber("POL-2024-0001")
.withStatus("PENDING_REVIEW")
.withRequestedAmount(new BigDecimal("1200.00"));
LegacyClaimFacadeBean facade = new LegacyClaimFacadeBean(
claimDao,
auditDao,
new FakeLegacyDocumentShareClient()
.verified("CLM-10001", "DAMAGE_PHOTO")
.verified("CLM-10001", "INVOICE"),
new FakeFraudRiskSoapClient().withScore("CLM-10001", 12),
new FakePolicyCoverageSoapClient()
.covered("POL-2024-0001", new BigDecimal("250.00"), new BigDecimal("5000.00")),
new FakeLegacyPaymentSoapClient().paymentPrepared("CLM-10001"),
eventPublisher
);
String resultStatus = facade.processClaimDecision(
"CLM-10001",
"APPROVE",
"claims.manager",
List.of("CLAIMS_MANAGER"),
"approved after document review",
false
);
assertThat(resultStatus).isEqualTo("PAYMENT_PENDING");
assertThat(claimDao.statusOf("CLM-10001")).isEqualTo("PAYMENT_PENDING");
assertThat(auditDao.eventsFor("CLM-10001"))
.containsExactly(
"CLAIM_DECISION_STARTED",
"COVERAGE_CHECKED",
"FRAUD_CHECKED",
"DOCUMENTS_VERIFIED",
"PAYMENT_PREPARED",
"CLAIM_APPROVED"
);
assertThat(eventPublisher.events())
.extracting("type")
.contains("CLAIM_APPROVED", "PAYMENT_PREPARED");
}
}Warum ist dieser Test absichtlich noch nicht schön?
Der Test kennt noch Legacy-Namen und Legacy-Statusstrings. Das ist am Anfang okay. Er dient als Schutznetz beim Schneiden der Monster Method.
Wichtig ist:
- Wir können Verhalten reproduzieren.
- Wir sehen Audit Events.
- Wir sehen JMS-Nachrichten.
- Wir sehen Statusänderungen.
- Wir können später vergleichen, ob der neue Handler fachlich gleich reagiert.
Golden Master Snapshot
Datei:
migration-tests/src/test/java/com/seb4u/demo/claims/migration/GoldenMasterSnapshot.javapackage com.seb4u.demo.claims.migration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class GoldenMasterSnapshot {
private final String caseId;
private final String claimId;
private final String finalStatus;
private final List<String> auditEvents;
private final List<String> outboundEvents;
private final List<String> storedProcedureCalls;
private final List<String> externalSoapCalls;
private GoldenMasterSnapshot(Builder builder) {
this.caseId = builder.caseId;
this.claimId = builder.claimId;
this.finalStatus = builder.finalStatus;
this.auditEvents = List.copyOf(builder.auditEvents);
this.outboundEvents = List.copyOf(builder.outboundEvents);
this.storedProcedureCalls = List.copyOf(builder.storedProcedureCalls);
this.externalSoapCalls = List.copyOf(builder.externalSoapCalls);
}
public String caseId() {
return caseId;
}
public String claimId() {
return claimId;
}
public String finalStatus() {
return finalStatus;
}
public List<String> auditEvents() {
return Collections.unmodifiableList(auditEvents);
}
public List<String> outboundEvents() {
return Collections.unmodifiableList(outboundEvents);
}
public List<String> storedProcedureCalls() {
return Collections.unmodifiableList(storedProcedureCalls);
}
public List<String> externalSoapCalls() {
return Collections.unmodifiableList(externalSoapCalls);
}
public static Builder builder(String caseId, String claimId) {
return new Builder(caseId, claimId);
}
public static final class Builder {
private final String caseId;
private final String claimId;
private String finalStatus;
private final List<String> auditEvents = new ArrayList<>();
private final List<String> outboundEvents = new ArrayList<>();
private final List<String> storedProcedureCalls = new ArrayList<>();
private final List<String> externalSoapCalls = new ArrayList<>();
private Builder(String caseId, String claimId) {
this.caseId = caseId;
this.claimId = claimId;
}
public Builder finalStatus(String finalStatus) {
this.finalStatus = finalStatus;
return this;
}
public Builder auditEvent(String auditEvent) {
this.auditEvents.add(auditEvent);
return this;
}
public Builder outboundEvent(String outboundEvent) {
this.outboundEvents.add(outboundEvent);
return this;
}
public Builder storedProcedureCall(String storedProcedureCall) {
this.storedProcedureCalls.add(storedProcedureCall);
return this;
}
public Builder externalSoapCall(String externalSoapCall) {
this.externalSoapCalls.add(externalSoapCall);
return this;
}
public GoldenMasterSnapshot build() {
return new GoldenMasterSnapshot(this);
}
}
}Was ist daran wichtig?
Ein Golden Master Snapshot speichert nicht jedes technische Detail. Er speichert die fachlich und migrationsrelevant wichtigen Beobachtungen:
- Endstatus
- Audit Trail
- externe Aufrufe
- technische Nebenwirkungen
- Outbound Events
- Stored-Procedure-Aufrufe
Damit lässt sich später vergleichen:
Reagiert der neue modulare Handler fachlich gleich, obwohl die interne Struktur komplett anders ist?
Teil B: Erste Schnitte an der Monster Method
Schritt 02: Refactoring Seam einführen
Was war vorher schlecht?
Die Legacy-Methode hat viele Parameter, direkte Infrastrukturzugriffe und keinen klaren Einstiegspunkt für Tests.
Typischer Legacy-Ausschnitt:
public String processClaimDecision(
String claimId,
String decision,
String actorUserId,
List<String> actorRoles,
String comment,
boolean forceApproval) throws Exception {
// 800+ Zeilen Fachlogik, technische Logik und Nebenwirkungen
}Was wird geändert?
Wir ändern zunächst nicht den fachlichen Ablauf. Wir führen nur eine interne Methode ein, die später herausgezogen werden kann.
Datei:
before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimFacadeBean.javapackage com.seb4u.demo.claims.legacy.ejb;
import java.util.List;
public class LegacyClaimFacadeBean {
public String processClaimDecision(
String claimId,
String decision,
String actorUserId,
List<String> actorRoles,
String comment,
boolean forceApproval) throws Exception {
LegacyDecisionInput input = LegacyDecisionInput.from(
claimId,
decision,
actorUserId,
actorRoles,
comment,
forceApproval
);
return processClaimDecisionInternal(input);
}
String processClaimDecisionInternal(LegacyDecisionInput input) throws Exception {
// In Schritt 02 enthält diese Methode noch fast den gesamten alten Code.
// Der Unterschied: Der Ablauf hat jetzt einen testbaren Eingabecontainer.
// Weitere Schnitte können nun kleiner und kontrollierter durchgeführt werden.
return legacyProcessClaimDecisionStillMostlyUnchanged(input);
}
private String legacyProcessClaimDecisionStillMostlyUnchanged(LegacyDecisionInput input) throws Exception {
// Platzhalter für die bestehende Monster Method aus Runde 2.
// In der echten Codebasis würde der bestehende Code hierher verschoben,
// ohne die Logik fachlich zu ändern.
throw new UnsupportedOperationException("legacy body moved here during refactoring seam step");
}
}Datei:
before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyDecisionInput.javapackage com.seb4u.demo.claims.legacy.ejb;
import java.util.List;
import java.util.Objects;
final class LegacyDecisionInput {
private final String claimId;
private final String decision;
private final String actorUserId;
private final List<String> actorRoles;
private final String comment;
private final boolean forceApproval;
private LegacyDecisionInput(
String claimId,
String decision,
String actorUserId,
List<String> actorRoles,
String comment,
boolean forceApproval) {
this.claimId = requireText(claimId, "claimId");
this.decision = requireText(decision, "decision");
this.actorUserId = requireText(actorUserId, "actorUserId");
this.actorRoles = List.copyOf(Objects.requireNonNull(actorRoles, "actorRoles"));
this.comment = comment == null ? "" : comment.trim();
this.forceApproval = forceApproval;
}
static LegacyDecisionInput from(
String claimId,
String decision,
String actorUserId,
List<String> actorRoles,
String comment,
boolean forceApproval) {
return new LegacyDecisionInput(claimId, decision, actorUserId, actorRoles, comment, forceApproval);
}
String claimId() {
return claimId;
}
String decision() {
return decision;
}
String actorUserId() {
return actorUserId;
}
List<String> actorRoles() {
return actorRoles;
}
String comment() {
return comment;
}
boolean forceApproval() {
return forceApproval;
}
private static String requireText(String value, String fieldName) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(fieldName + " must not be blank");
}
return value.trim();
}
}Warum wurde es geändert?
Damit die ursprüngliche Signatur stabil bleibt, aber intern ein kontrollierter Einstiegspunkt entsteht.
Pattern / Konzept
- Refactoring Seam
- Parameter Object
- Sprout Method
Tests
Der vorhandene Characterization Test muss weiterhin grün bleiben.
Risiko
Schon das Verschieben von Code kann Seiteneffekte verändern, wenn Initialisierung, Transaktionen oder Lazy Loading betroffen sind. Deshalb darf in diesem Schritt keine fachliche Logik verändert werden.
Schritt 03: Command Object extrahieren
Was war vorher schlecht?
Die Parameterliste repräsentiert eigentlich einen fachlichen Auftrag. Im Legacy-Code bleibt das unsichtbar.
Was wird geändert?
Aus LegacyDecisionInput wird später ein modernes
ProcessClaimDecisionCommand.
Datei:
after_refactoring_modular/claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionCommand.javapackage com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.DecisionComment;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.ActorId;
import com.seb4u.demo.claims.shared.security.ActorRole;
import java.time.Instant;
import java.util.Set;
public final class ProcessClaimDecisionCommand {
private final ClaimId claimId;
private final RequestedDecision requestedDecision;
private final ActorId actorId;
private final Set<ActorRole> actorRoles;
private final DecisionComment comment;
private final boolean forceApproval;
private final Instant requestedAt;
private final String correlationId;
private ProcessClaimDecisionCommand(Builder builder) {
this.claimId = builder.claimId;
this.requestedDecision = builder.requestedDecision;
this.actorId = builder.actorId;
this.actorRoles = Set.copyOf(builder.actorRoles);
this.comment = builder.comment;
this.forceApproval = builder.forceApproval;
this.requestedAt = builder.requestedAt;
this.correlationId = builder.correlationId;
}
public ClaimId claimId() {
return claimId;
}
public RequestedDecision requestedDecision() {
return requestedDecision;
}
public ActorId actorId() {
return actorId;
}
public Set<ActorRole> actorRoles() {
return actorRoles;
}
public DecisionComment comment() {
return comment;
}
public boolean forceApproval() {
return forceApproval;
}
public Instant requestedAt() {
return requestedAt;
}
public String correlationId() {
return correlationId;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private ClaimId claimId;
private RequestedDecision requestedDecision;
private ActorId actorId;
private Set<ActorRole> actorRoles = Set.of();
private DecisionComment comment = DecisionComment.empty();
private boolean forceApproval;
private Instant requestedAt = Instant.now();
private String correlationId;
public Builder claimId(ClaimId claimId) {
this.claimId = claimId;
return this;
}
public Builder requestedDecision(RequestedDecision requestedDecision) {
this.requestedDecision = requestedDecision;
return this;
}
public Builder actorId(ActorId actorId) {
this.actorId = actorId;
return this;
}
public Builder actorRoles(Set<ActorRole> actorRoles) {
this.actorRoles = actorRoles;
return this;
}
public Builder comment(DecisionComment comment) {
this.comment = comment;
return this;
}
public Builder forceApproval(boolean forceApproval) {
this.forceApproval = forceApproval;
return this;
}
public Builder requestedAt(Instant requestedAt) {
this.requestedAt = requestedAt;
return this;
}
public Builder correlationId(String correlationId) {
this.correlationId = correlationId;
return this;
}
public ProcessClaimDecisionCommand build() {
if (claimId == null) {
throw new IllegalStateException("claimId is required");
}
if (requestedDecision == null) {
throw new IllegalStateException("requestedDecision is required");
}
if (actorId == null) {
throw new IllegalStateException("actorId is required");
}
if (correlationId == null || correlationId.isBlank()) {
throw new IllegalStateException("correlationId is required");
}
return new ProcessClaimDecisionCommand(this);
}
}
}Warum wurde es geändert?
Der Use Case bekommt einen fachlichen Namen. Später kann derselbe Command aus SOAP, REST, Batch oder Backoffice kommen.
Pattern / Konzept
- Command Pattern
- Application Service Boundary
- API-unabhängiger Use Case
Tests
- Command Mapping Test von SOAP nach Command
- Command Mapping Test von REST nach Command
- Handler Test mit Command
Risiko
Zu frühe Perfektion bei Value Objects kann Refactoring verlangsamen. Deshalb werden zunächst nur wichtige Fachwerte typisiert.
Schritt 04: Ergebnisobjekt einführen
Was war vorher schlecht?
Die Legacy-Methode gibt einen String zurück oder wirft Exceptions. Dadurch ist nicht klar:
- Wurde abgelehnt?
- Wurde nur in manuelle Prüfung gestellt?
- Muss eine Zahlung vorbereitet werden?
- Muss eine Notification gesendet werden?
- Ist ein technischer Fehler passiert?
Was wird geändert?
Ein fachliches Ergebnisobjekt wird eingeführt.
Datei:
claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionResult.javapackage com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.DecisionReason;
import java.util.ArrayList;
import java.util.List;
public final class ProcessClaimDecisionResult {
private final ClaimId claimId;
private final ClaimStatus previousStatus;
private final ClaimStatus newStatus;
private final DecisionReason reason;
private final boolean paymentPreparationRequired;
private final boolean customerNotificationRequired;
private final boolean managerEscalationRequired;
private final List<String> emittedDomainEvents;
private ProcessClaimDecisionResult(Builder builder) {
this.claimId = builder.claimId;
this.previousStatus = builder.previousStatus;
this.newStatus = builder.newStatus;
this.reason = builder.reason;
this.paymentPreparationRequired = builder.paymentPreparationRequired;
this.customerNotificationRequired = builder.customerNotificationRequired;
this.managerEscalationRequired = builder.managerEscalationRequired;
this.emittedDomainEvents = List.copyOf(builder.emittedDomainEvents);
}
public ClaimId claimId() {
return claimId;
}
public ClaimStatus previousStatus() {
return previousStatus;
}
public ClaimStatus newStatus() {
return newStatus;
}
public DecisionReason reason() {
return reason;
}
public boolean paymentPreparationRequired() {
return paymentPreparationRequired;
}
public boolean customerNotificationRequired() {
return customerNotificationRequired;
}
public boolean managerEscalationRequired() {
return managerEscalationRequired;
}
public List<String> emittedDomainEvents() {
return emittedDomainEvents;
}
public static Builder builder(ClaimId claimId) {
return new Builder(claimId);
}
public static final class Builder {
private final ClaimId claimId;
private ClaimStatus previousStatus;
private ClaimStatus newStatus;
private DecisionReason reason;
private boolean paymentPreparationRequired;
private boolean customerNotificationRequired;
private boolean managerEscalationRequired;
private final List<String> emittedDomainEvents = new ArrayList<>();
private Builder(ClaimId claimId) {
this.claimId = claimId;
}
public Builder previousStatus(ClaimStatus previousStatus) {
this.previousStatus = previousStatus;
return this;
}
public Builder newStatus(ClaimStatus newStatus) {
this.newStatus = newStatus;
return this;
}
public Builder reason(DecisionReason reason) {
this.reason = reason;
return this;
}
public Builder paymentPreparationRequired(boolean value) {
this.paymentPreparationRequired = value;
return this;
}
public Builder customerNotificationRequired(boolean value) {
this.customerNotificationRequired = value;
return this;
}
public Builder managerEscalationRequired(boolean value) {
this.managerEscalationRequired = value;
return this;
}
public Builder emit(String eventType) {
this.emittedDomainEvents.add(eventType);
return this;
}
public ProcessClaimDecisionResult build() {
if (previousStatus == null || newStatus == null || reason == null) {
throw new IllegalStateException("previousStatus, newStatus and reason are required");
}
return new ProcessClaimDecisionResult(this);
}
}
}Warum wurde es geändert?
Das Ergebnis wird fachlich erklärbar. SOAP kann daraus weiterhin einen String oder eine alte Response erzeugen. REST kann daraus eine moderne JSON-Antwort erzeugen.
Pattern / Konzept
- Result Object
- Application Boundary
- Fachliche Fehler statt technischer Exceptions
Tests
- Result Mapping Test
- SOAP Response Compatibility Test
- REST Response Test
Risiko
Legacy-Clients können alte Fehlercodes erwarten. Deshalb darf die SOAP-Schicht zunächst weiterhin Legacy-kompatibel mappen.
Teil C: Fachmodell und Statuslogik
Schritt 05: Fachliche Value Objects einführen
Was war vorher schlecht?
Im Legacy-Code sind viele Fachwerte einfache Strings:
claimIddecisionstatusactorUserIdpolicyNumber
Dadurch entstehen Fehler wie:
- Tippfehler in Statuswerten
- falsche Vergleiche
- leere IDs
- technische Werte in Fachlogik
Was wird geändert?
Wichtige Fachbegriffe werden als Value Objects modelliert.
Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/ClaimId.javapackage com.seb4u.demo.claims.domain.model;
import java.util.Objects;
import java.util.regex.Pattern;
public final class ClaimId {
private static final Pattern FORMAT = Pattern.compile("CLM-[0-9]{5,12}");
private final String value;
private ClaimId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("claimId must not be blank");
}
String normalized = value.trim().toUpperCase();
if (!FORMAT.matcher(normalized).matches()) {
throw new IllegalArgumentException("invalid claimId format: " + value);
}
this.value = normalized;
}
public static ClaimId of(String value) {
return new ClaimId(value);
}
public String value() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ClaimId claimId)) return false;
return value.equals(claimId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/RequestedDecision.javapackage com.seb4u.demo.claims.domain.model;
public enum RequestedDecision {
APPROVE,
REJECT,
ESCALATE,
REQUEST_MORE_DOCUMENTS,
SEND_TO_FRAUD_REVIEW;
public static RequestedDecision fromLegacyCode(String code) {
if (code == null || code.isBlank()) {
throw new IllegalArgumentException("decision must not be blank");
}
return switch (code.trim().toUpperCase()) {
case "APPROVE", "APPROVED" -> APPROVE;
case "REJECT", "REJECTED" -> REJECT;
case "ESCALATE", "ESCALATED" -> ESCALATE;
case "REQ_DOC", "REQUEST_MORE_DOCUMENTS" -> REQUEST_MORE_DOCUMENTS;
case "FRAUD_REVIEW", "SEND_TO_FRAUD_REVIEW" -> SEND_TO_FRAUD_REVIEW;
default -> throw new IllegalArgumentException("unsupported decision: " + code);
};
}
}Pattern / Konzept
- Value Object
- Ubiquitous Language
- Anti-Corruption Mapping für Legacy Codes
Tests
Datei:
claim-domain/src/test/java/com/seb4u/demo/claims/domain/model/RequestedDecisionTest.javapackage com.seb4u.demo.claims.domain.model;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class RequestedDecisionTest {
@Test
void mapsLegacyApproveCodes() {
assertThat(RequestedDecision.fromLegacyCode("APPROVE"))
.isEqualTo(RequestedDecision.APPROVE);
assertThat(RequestedDecision.fromLegacyCode("APPROVED"))
.isEqualTo(RequestedDecision.APPROVE);
}
@Test
void rejectsUnknownLegacyCode() {
assertThatThrownBy(() -> RequestedDecision.fromLegacyCode("PAY_NOW_AND_IGNORE_FRAUD"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("unsupported decision");
}
}Schritt 06: Statuslogik in Policy überführen
Was war vorher schlecht?
Im Legacy-Code steht Statuslogik verteilt:
if ("PENDING_REVIEW".equals(status) && "APPROVE".equals(decision)) {
if (fraudScore < 80 && coverageCovered && allDocsVerified) {
status = "PAYMENT_PENDING";
} else if (fraudScore >= 80) {
status = "MANUAL_FRAUD_REVIEW";
} else if (!coverageCovered) {
status = "REJECTED";
} else {
status = "WAITING_FOR_DOCUMENTS";
}
} else if ("WAITING_FOR_DOCUMENTS".equals(status) && "APPROVE".equals(decision)) {
// weitere Sonderfälle
}Das Problem:
- Statusregeln sind schwer auffindbar.
- Reihenfolge der Bedingungen ist kritisch.
- Tests sind schwer zu schreiben.
- Fachregeln werden mit technischen Calls vermischt.
Was wird geändert?
Statusübergänge werden in ClaimTransitionPolicy
gekapselt.
Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicy.javapackage com.seb4u.demo.claims.domain.workflow;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.DecisionReason;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.model.TransitionDecision;
public final class ClaimTransitionPolicy {
public TransitionDecision decide(ClaimTransitionContext context) {
if (context.currentStatus() == ClaimStatus.CLOSED) {
return TransitionDecision.rejectTransition(
context.currentStatus(),
DecisionReason.of("closed claims cannot be changed")
);
}
if (context.requestedDecision() == RequestedDecision.REQUEST_MORE_DOCUMENTS) {
return TransitionDecision.moveTo(
ClaimStatus.WAITING_FOR_DOCUMENTS,
DecisionReason.of("additional documents requested")
);
}
if (!context.authorization().mayProcessDecision()) {
return TransitionDecision.rejectTransition(
context.currentStatus(),
DecisionReason.of("actor is not allowed to process claim decision")
);
}
if (!context.documents().allRequiredDocumentsVerified()) {
return TransitionDecision.moveTo(
ClaimStatus.WAITING_FOR_DOCUMENTS,
DecisionReason.of("required documents are missing or not verified")
);
}
if (context.fraudRisk().requiresManualReview()) {
return TransitionDecision.moveTo(
ClaimStatus.MANUAL_FRAUD_REVIEW,
DecisionReason.of("fraud score requires manual review")
);
}
if (!context.coverage().covered()) {
return TransitionDecision.moveTo(
ClaimStatus.REJECTED,
DecisionReason.of("claim is not covered by policy")
);
}
return switch (context.requestedDecision()) {
case APPROVE -> TransitionDecision.moveTo(
ClaimStatus.PAYMENT_PENDING,
DecisionReason.of("claim approved and payment preparation required")
);
case REJECT -> TransitionDecision.moveTo(
ClaimStatus.REJECTED,
DecisionReason.of("claim rejected by authorized actor")
);
case ESCALATE -> TransitionDecision.moveTo(
ClaimStatus.ESCALATED,
DecisionReason.of("claim escalated by actor")
);
case SEND_TO_FRAUD_REVIEW -> TransitionDecision.moveTo(
ClaimStatus.MANUAL_FRAUD_REVIEW,
DecisionReason.of("manual fraud review requested")
);
case REQUEST_MORE_DOCUMENTS -> TransitionDecision.moveTo(
ClaimStatus.WAITING_FOR_DOCUMENTS,
DecisionReason.of("additional documents requested")
);
};
}
}Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionContext.javapackage com.seb4u.demo.claims.domain.workflow;
import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
public record ClaimTransitionContext(
ClaimStatus currentStatus,
RequestedDecision requestedDecision,
AuthorizationDecision authorization,
DocumentVerificationSummary documents,
FraudRiskAssessment fraudRisk,
CoverageDecision coverage
) {
}Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/TransitionDecision.javapackage com.seb4u.demo.claims.domain.model;
public final class TransitionDecision {
private final boolean allowed;
private final ClaimStatus targetStatus;
private final DecisionReason reason;
private TransitionDecision(boolean allowed, ClaimStatus targetStatus, DecisionReason reason) {
this.allowed = allowed;
this.targetStatus = targetStatus;
this.reason = reason;
}
public static TransitionDecision moveTo(ClaimStatus targetStatus, DecisionReason reason) {
return new TransitionDecision(true, targetStatus, reason);
}
public static TransitionDecision rejectTransition(ClaimStatus currentStatus, DecisionReason reason) {
return new TransitionDecision(false, currentStatus, reason);
}
public boolean allowed() {
return allowed;
}
public ClaimStatus targetStatus() {
return targetStatus;
}
public DecisionReason reason() {
return reason;
}
}Tests
Datei:
claim-domain/src/test/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicyTest.javapackage com.seb4u.demo.claims.domain.workflow;
import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.model.TransitionDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ClaimTransitionPolicyTest {
private final ClaimTransitionPolicy policy = new ClaimTransitionPolicy();
@Test
void movesApprovedCoveredLowRiskClaimToPaymentPending() {
ClaimTransitionContext context = new ClaimTransitionContext(
ClaimStatus.PENDING_REVIEW,
RequestedDecision.APPROVE,
AuthorizationDecision.allowed("manager may approve"),
DocumentVerificationSummary.allRequiredVerified(),
FraudRiskAssessment.lowRisk(12),
CoverageDecision.covered()
);
TransitionDecision decision = policy.decide(context);
assertThat(decision.allowed()).isTrue();
assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.PAYMENT_PENDING);
assertThat(decision.reason().value()).contains("payment preparation required");
}
@Test
void sendsHighRiskClaimToManualFraudReviewBeforePayment() {
ClaimTransitionContext context = new ClaimTransitionContext(
ClaimStatus.PENDING_REVIEW,
RequestedDecision.APPROVE,
AuthorizationDecision.allowed("manager may approve"),
DocumentVerificationSummary.allRequiredVerified(),
FraudRiskAssessment.highRisk(91),
CoverageDecision.covered()
);
TransitionDecision decision = policy.decide(context);
assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.MANUAL_FRAUD_REVIEW);
assertThat(decision.reason().value()).contains("fraud score");
}
@Test
void waitsForDocumentsWhenRequiredDocumentsAreMissing() {
ClaimTransitionContext context = new ClaimTransitionContext(
ClaimStatus.PENDING_REVIEW,
RequestedDecision.APPROVE,
AuthorizationDecision.allowed("manager may approve"),
DocumentVerificationSummary.missingRequired("INVOICE"),
FraudRiskAssessment.lowRisk(10),
CoverageDecision.covered()
);
TransitionDecision decision = policy.decide(context);
assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.WAITING_FOR_DOCUMENTS);
}
}Warum wurde es geändert?
Jetzt kann die Statuslogik ohne WebSphere, SOAP, Oracle, JMS oder File Share getestet werden.
Pattern / Konzept
- State Pattern
- Policy Object
- Pure Domain Logic
Risiko
Die Reihenfolge der alten if/else-Regeln kann fachlich relevant gewesen sein. Deshalb muss jeder neue Policy-Test gegen Golden-Master-Szenarien geprüft werden.
Teil D: Ports & Adapters extrahieren
Schritt 07: Document Verification als Port
Was war vorher schlecht?
Dokumente wurden direkt in der EJB aus File Shares und SOAP-Metadaten geprüft.
Typische Legacy-Probleme:
- hart verdrahtete Pfade wie
/claims/documents/incoming/ - XML-Metadaten direkt in Fachlogik
- technische FileNotFoundException wird fachlich als fehlendes Dokument interpretiert
- Versionierung und Archivierung sind vermischt
Was wird geändert?
Die Anwendung kennt nur noch Ports.
Datei:
claim-document/src/main/java/com/seb4u/demo/claims/document/DocumentVerificationPort.javapackage com.seb4u.demo.claims.document;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;
public interface DocumentVerificationPort {
DocumentVerificationSummary verifyRequiredDocuments(ClaimId claimId);
}Datei:
claim-document/src/main/java/com/seb4u/demo/claims/document/FileShareDocumentVerificationAdapter.javapackage com.seb4u.demo.claims.document;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public final class FileShareDocumentVerificationAdapter implements DocumentVerificationPort {
private final Path rootDirectory;
private final LegacyDocumentMetadataReader metadataReader;
public FileShareDocumentVerificationAdapter(Path rootDirectory, LegacyDocumentMetadataReader metadataReader) {
this.rootDirectory = rootDirectory;
this.metadataReader = metadataReader;
}
@Override
public DocumentVerificationSummary verifyRequiredDocuments(ClaimId claimId) {
Path claimFolder = rootDirectory.resolve(claimId.value());
if (!Files.exists(claimFolder)) {
return DocumentVerificationSummary.missingRequired("CLAIM_FOLDER");
}
List<String> missing = new ArrayList<>();
verifyDocument(claimFolder, "DAMAGE_PHOTO", missing);
verifyDocument(claimFolder, "INVOICE", missing);
if (!missing.isEmpty()) {
return DocumentVerificationSummary.missingRequired(missing);
}
LegacyDocumentMetadata metadata = metadataReader.readMetadata(claimFolder);
if (!metadata.allDocumentsVirusChecked()) {
return DocumentVerificationSummary.notVerified("virus scan missing");
}
if (!metadata.allDocumentsAssignedToClaim(claimId.value())) {
return DocumentVerificationSummary.notVerified("metadata claim assignment mismatch");
}
return DocumentVerificationSummary.allRequiredVerified();
}
private void verifyDocument(Path claimFolder, String documentType, List<String> missing) {
Path file = claimFolder.resolve(documentType + ".pdf");
if (!Files.exists(file)) {
missing.add(documentType);
}
}
}Pattern / Konzept
- Port
- Adapter
- Anti-Corruption Layer
Tests
package com.seb4u.demo.claims.document;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FileShareDocumentVerificationAdapterTest {
@TempDir
Path tempDir;
@Test
void returnsMissingDocumentsWhenInvoiceIsAbsent() throws Exception {
ClaimId claimId = ClaimId.of("CLM-12345");
Path claimFolder = Files.createDirectories(tempDir.resolve(claimId.value()));
Files.writeString(claimFolder.resolve("DAMAGE_PHOTO.pdf"), "fake-pdf");
FileShareDocumentVerificationAdapter adapter = new FileShareDocumentVerificationAdapter(
tempDir,
folder -> LegacyDocumentMetadata.validFor(claimId.value())
);
DocumentVerificationSummary summary = adapter.verifyRequiredDocuments(claimId);
assertThat(summary.allRequiredDocumentsVerified()).isFalse();
assertThat(summary.missingDocumentTypes()).contains("INVOICE");
}
}Risiko
File-System-Adapter können auf OpenShift problematisch sein, wenn sie lokale Pfade erwarten. Deshalb wird der Port so geschnitten, dass später Object Storage oder ein DMS-Service eingebunden werden kann.
Schritt 08: Fraud Risk als Port
Datei:
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/FraudRiskPort.javapackage com.seb4u.demo.claims.application.ports;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
public interface FraudRiskPort {
FraudRiskAssessment assessRisk(ClaimId claimId, CustomerSnapshot customerSnapshot);
}Datei:
claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/fraud/FraudRiskSoapAdapter.javapackage com.seb4u.demo.claims.infrastructure.fraud;
import com.seb4u.demo.claims.application.ports.FraudRiskPort;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimId;
public final class FraudRiskSoapAdapter implements FraudRiskPort {
private final FraudRiskSoapClient client;
private final FraudRiskMapper mapper;
private final FraudRiskTimeoutPolicy timeoutPolicy;
public FraudRiskSoapAdapter(
FraudRiskSoapClient client,
FraudRiskMapper mapper,
FraudRiskTimeoutPolicy timeoutPolicy) {
this.client = client;
this.mapper = mapper;
this.timeoutPolicy = timeoutPolicy;
}
@Override
public FraudRiskAssessment assessRisk(ClaimId claimId, CustomerSnapshot customerSnapshot) {
FraudRiskRequest request = mapper.toRequest(claimId, customerSnapshot);
try {
FraudRiskResponse response = timeoutPolicy.execute(
() -> client.calculateRisk(request),
"fraud-risk",
claimId.value()
);
return mapper.toDomain(response);
} catch (FraudRiskTimeoutException timeout) {
return FraudRiskAssessment.unknownRequiresManualReview(
"fraud risk timeout: " + timeout.getMessage()
);
} catch (FraudRiskTechnicalException technical) {
return FraudRiskAssessment.unknownRequiresManualReview(
"fraud risk technical error: " + technical.getMessage()
);
}
}
}Was wurde verbessert?
- Die Domain sieht keinen SOAP-Client mehr.
- Timeout wird nicht mehr als zufälliger technischer Fehler in der EJB behandelt.
- Technischer Fehler wird in eine konservative fachliche Entscheidung übersetzt: manuelle Prüfung.
Pattern / Konzept
- Port & Adapter
- Anti-Corruption Layer
- Timeout Policy
- Conservative Fallback
Risiko
Nicht jeder technische Fehler darf fachlich gleich behandelt werden. Deshalb muss mit Fachbereich und Betrieb geklärt werden:
- Wann wird manuelle Prüfung ausgelöst?
- Wann wird der Prozess gestoppt?
- Wann darf automatisch erneut versucht werden?
Schritt 09: Coverage als Specification/Policy
Was war vorher schlecht?
Coverage wurde direkt über SOAP/Stored Procedure geprüft. Die alte Tariflogik war schwer lesbar.
Was wird geändert?
Technische Coverage-Ermittlung und fachliche Coverage-Entscheidung werden getrennt.
Datei:
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CoveragePort.javapackage com.seb4u.demo.claims.application.ports;
import com.seb4u.demo.claims.domain.coverage.CoverageSnapshot;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.policy.PolicyNumber;
public interface CoveragePort {
CoverageSnapshot loadCoverage(ClaimId claimId, PolicyNumber policyNumber);
}Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/coverage/CoveragePolicy.javapackage com.seb4u.demo.claims.domain.coverage;
import com.seb4u.demo.claims.domain.model.ClaimAmount;
public final class CoveragePolicy {
public CoverageDecision decide(CoverageSnapshot snapshot, ClaimAmount requestedAmount) {
if (!snapshot.policyActive()) {
return CoverageDecision.notCovered("policy is not active");
}
if (snapshot.exclusionCodes().contains("INTENTIONAL_DAMAGE")) {
return CoverageDecision.notCovered("intentional damage exclusion applies");
}
if (requestedAmount.isGreaterThan(snapshot.maxPayableAmount())) {
return CoverageDecision.partiallyCovered(
snapshot.maxPayableAmount(),
"requested amount exceeds maximum payable amount"
);
}
return CoverageDecision.covered(snapshot.deductibleAmount(), requestedAmount);
}
}Pattern / Konzept
- Specification Pattern
- Policy Object
- Separation of Data Retrieval and Decision Logic
Test
package com.seb4u.demo.claims.domain.coverage;
import com.seb4u.demo.claims.domain.model.ClaimAmount;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class CoveragePolicyTest {
@Test
void rejectsClaimWhenPolicyIsInactive() {
CoverageSnapshot snapshot = new CoverageSnapshot(
false,
new BigDecimal("0.00"),
new BigDecimal("5000.00"),
Set.of()
);
CoverageDecision decision = new CoveragePolicy()
.decide(snapshot, ClaimAmount.of("1200.00"));
assertThat(decision.covered()).isFalse();
assertThat(decision.reason()).contains("not active");
}
}Schritt 10: Payment Release als Port plus Idempotency
Was war vorher schlecht?
Im Legacy-Code wurde Payment innerhalb der gleichen JTA-Transaktion wie Claim-Status, Audit und JMS vorbereitet. Das ist gefährlich:
- Zahlung kann vorbereitet sein, obwohl Claim-Status nicht sauber gespeichert wurde.
- Wiederholung kann Doppelzahlung auslösen.
- SOAP Timeout kann unklar sein: Zahlung vielleicht ausgelöst, Antwort aber verloren.
Was wird geändert?
Payment wird über einen Port angesprochen und mit Idempotency Key abgesichert.
Datei:
claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentReleasePort.javapackage com.seb4u.demo.claims.payment;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.payment.PaymentPreparationResult;
public interface PaymentReleasePort {
PaymentPreparationResult preparePayment(PaymentPreparationRequest request);
PaymentPreparationResult findByIdempotencyKey(IdempotencyKey idempotencyKey);
}Datei:
claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentPreparationRequest.javapackage com.seb4u.demo.claims.payment;
import com.seb4u.demo.claims.domain.model.ClaimAmount;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.customer.CustomerId;
public record PaymentPreparationRequest(
ClaimId claimId,
CustomerId customerId,
ClaimAmount amount,
IdempotencyKey idempotencyKey,
String reason
) {
}Datei:
claim-payment/src/main/java/com/seb4u/demo/claims/payment/IdempotencyKey.javapackage com.seb4u.demo.claims.payment;
import com.seb4u.demo.claims.domain.model.ClaimId;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class IdempotencyKey {
private final String value;
private IdempotencyKey(String value) {
this.value = value;
}
public static IdempotencyKey forPaymentPreparation(ClaimId claimId, String decisionVersion) {
return new IdempotencyKey("PAYMENT_PREPARE-" + sha256(claimId.value() + ":" + decisionVersion));
}
public String value() {
return value;
}
private static String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
}Pattern / Konzept
- Port & Adapter
- Idempotency
- Compensation
- Transaction Boundary
Risiko
Idempotency muss mit dem Zielsystem abgestimmt werden. Wenn das alte Payment-System keinen Idempotency Key unterstützt, muss der Adapter lokal eine Idempotency-Tabelle führen.
Teil E: Customer und Identity entkoppeln
Schritt 11: Customer Master Data Port
Datei:
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CustomerMasterDataPort.javapackage com.seb4u.demo.claims.application.ports;
import com.seb4u.demo.claims.domain.customer.CustomerId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
public interface CustomerMasterDataPort {
CustomerSnapshot loadCustomer(CustomerId customerId);
}Datei:
claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/customer/CustomerMasterDataSoapAdapter.javapackage com.seb4u.demo.claims.infrastructure.customer;
import com.seb4u.demo.claims.application.ports.CustomerMasterDataPort;
import com.seb4u.demo.claims.domain.customer.CustomerId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
public final class CustomerMasterDataSoapAdapter implements CustomerMasterDataPort {
private final CustomerMasterDataSoapClient client;
private final CustomerMasterDataMapper mapper;
public CustomerMasterDataSoapAdapter(
CustomerMasterDataSoapClient client,
CustomerMasterDataMapper mapper) {
this.client = client;
this.mapper = mapper;
}
@Override
public CustomerSnapshot loadCustomer(CustomerId customerId) {
CustomerMasterDataResponse response = client.getCustomer(customerId.value());
return mapper.toSnapshot(response);
}
}Warum wurde es geändert?
Der Claim-Use-Case braucht eine stabile Momentaufnahme des Kunden, aber keine technische Abhängigkeit auf SOAP oder alte Kundennummernlogik.
Schritt 12: SecurityContextPort und AuthorizationPolicy
Was war vorher schlecht?
Rollen wurden im Legacy-Code direkt geprüft:
if (!roles.contains("CLAIMS_MANAGER") && forceApproval) {
throw new SecurityException("not allowed");
}Problem:
- Rollennamen sind hart kodiert.
- Partnerrollen und interne Rollen sind vermischt.
- WebSphere JAAS ist überall sichtbar.
- Migration zu modernem Identity Provider wird schwer.
Was wird geändert?
Authentifizierungskontext und fachliche Autorisierung werden getrennt.
Datei:
shared-kernel/src/main/java/com/seb4u/demo/claims/shared/security/SecurityContextPort.javapackage com.seb4u.demo.claims.shared.security;
public interface SecurityContextPort {
AuthenticatedActor currentActor();
}Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/security/AuthorizationPolicy.javapackage com.seb4u.demo.claims.domain.security;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.ActorRole;
public final class AuthorizationPolicy {
public AuthorizationDecision mayProcessDecision(
AuthenticatedActor actor,
ClaimStatus currentStatus,
RequestedDecision requestedDecision,
boolean forceApproval) {
if (actor.hasRole(ActorRole.SYSTEM_MIGRATION)) {
return AuthorizationDecision.allowed("system migration actor");
}
if (requestedDecision == RequestedDecision.APPROVE && forceApproval) {
return actor.hasRole(ActorRole.CLAIMS_MANAGER)
? AuthorizationDecision.allowed("manager may force approval")
: AuthorizationDecision.denied("force approval requires CLAIMS_MANAGER");
}
if (requestedDecision == RequestedDecision.REJECT) {
return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER)
? AuthorizationDecision.allowed("claims agent may reject")
: AuthorizationDecision.denied("reject requires internal claims role");
}
if (requestedDecision == RequestedDecision.REQUEST_MORE_DOCUMENTS) {
return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER, ActorRole.PARTNER_SUPPORT)
? AuthorizationDecision.allowed("actor may request documents")
: AuthorizationDecision.denied("requesting documents not allowed");
}
if (currentStatus == ClaimStatus.ESCALATED) {
return actor.hasRole(ActorRole.CLAIMS_MANAGER)
? AuthorizationDecision.allowed("manager may process escalated claim")
: AuthorizationDecision.denied("escalated claims require manager");
}
return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER)
? AuthorizationDecision.allowed("standard claims processing")
: AuthorizationDecision.denied("actor is not allowed to process claim decision");
}
}Pattern / Konzept
- Policy Object
- Security Context Port
- Role Mapping
- AuthN/AuthZ-Trennung
Tests
package com.seb4u.demo.claims.domain.security;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.ActorId;
import com.seb4u.demo.claims.shared.security.ActorRole;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class AuthorizationPolicyTest {
private final AuthorizationPolicy policy = new AuthorizationPolicy();
@Test
void deniesForceApprovalForClaimsAgent() {
AuthenticatedActor actor = new AuthenticatedActor(
ActorId.of("agent-1"),
Set.of(ActorRole.CLAIMS_AGENT)
);
AuthorizationDecision decision = policy.mayProcessDecision(
actor,
ClaimStatus.PENDING_REVIEW,
RequestedDecision.APPROVE,
true
);
assertThat(decision.allowed()).isFalse();
assertThat(decision.reason()).contains("CLAIMS_MANAGER");
}
}Risiko
Alte WebSphere-Rollen können anders gruppiert sein als moderne Identity-Provider-Rollen. Deshalb braucht die Migration ein eigenes Role-Mapping-Dokument.
Teil F: Workflow Orchestrator
Schritt 13: ProcessClaimDecisionHandler als Application Service
Was war vorher schlecht?
Die EJB war gleichzeitig technischer Entry Point und fachlicher Ablauf.
Was wird geändert?
Die eigentliche Use-Case-Logik wird in einen Application Handler verschoben.
Datei:
claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandler.javapackage com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.application.ports.ClaimRepository;
import com.seb4u.demo.claims.application.ports.CoveragePort;
import com.seb4u.demo.claims.application.ports.CustomerMasterDataPort;
import com.seb4u.demo.claims.application.ports.FraudRiskPort;
import com.seb4u.demo.claims.audit.AuditLogPort;
import com.seb4u.demo.claims.document.DocumentVerificationPort;
import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.coverage.CoveragePolicy;
import com.seb4u.demo.claims.domain.coverage.CoverageSnapshot;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.TransitionDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionContext;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionPolicy;
import com.seb4u.demo.claims.payment.PaymentPreparationRequest;
import com.seb4u.demo.claims.payment.PaymentReleasePort;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.SecurityContextPort;
public final class ProcessClaimDecisionHandler {
private final ClaimRepository claimRepository;
private final CustomerMasterDataPort customerMasterDataPort;
private final SecurityContextPort securityContextPort;
private final AuthorizationPolicy authorizationPolicy;
private final DocumentVerificationPort documentVerificationPort;
private final FraudRiskPort fraudRiskPort;
private final CoveragePort coveragePort;
private final CoveragePolicy coveragePolicy;
private final ClaimTransitionPolicy transitionPolicy;
private final PaymentReleasePort paymentReleasePort;
private final AuditLogPort auditLogPort;
private final ClaimDecisionOutbox outbox;
public ProcessClaimDecisionHandler(
ClaimRepository claimRepository,
CustomerMasterDataPort customerMasterDataPort,
SecurityContextPort securityContextPort,
AuthorizationPolicy authorizationPolicy,
DocumentVerificationPort documentVerificationPort,
FraudRiskPort fraudRiskPort,
CoveragePort coveragePort,
CoveragePolicy coveragePolicy,
ClaimTransitionPolicy transitionPolicy,
PaymentReleasePort paymentReleasePort,
AuditLogPort auditLogPort,
ClaimDecisionOutbox outbox) {
this.claimRepository = claimRepository;
this.customerMasterDataPort = customerMasterDataPort;
this.securityContextPort = securityContextPort;
this.authorizationPolicy = authorizationPolicy;
this.documentVerificationPort = documentVerificationPort;
this.fraudRiskPort = fraudRiskPort;
this.coveragePort = coveragePort;
this.coveragePolicy = coveragePolicy;
this.transitionPolicy = transitionPolicy;
this.paymentReleasePort = paymentReleasePort;
this.auditLogPort = auditLogPort;
this.outbox = outbox;
}
public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
AuthenticatedActor actor = securityContextPort.currentActor();
Claim claim = claimRepository.getById(command.claimId());
auditLogPort.append(command.claimId(), "CLAIM_DECISION_STARTED", actor.id().value(), command.correlationId());
AuthorizationDecision authorization = authorizationPolicy.mayProcessDecision(
actor,
claim.status(),
command.requestedDecision(),
command.forceApproval()
);
if (!authorization.allowed()) {
auditLogPort.append(command.claimId(), "CLAIM_DECISION_DENIED", actor.id().value(), command.correlationId());
return ProcessClaimDecisionResult.builder(command.claimId())
.previousStatus(claim.status())
.newStatus(claim.status())
.reason(authorization.asDecisionReason())
.managerEscalationRequired(false)
.customerNotificationRequired(false)
.paymentPreparationRequired(false)
.emit("CLAIM_DECISION_DENIED")
.build();
}
CustomerSnapshot customer = customerMasterDataPort.loadCustomer(claim.customerId());
DocumentVerificationSummary documents = documentVerificationPort.verifyRequiredDocuments(claim.id());
FraudRiskAssessment fraudRisk = fraudRiskPort.assessRisk(claim.id(), customer);
CoverageSnapshot coverageSnapshot = coveragePort.loadCoverage(claim.id(), claim.policyNumber());
CoverageDecision coverage = coveragePolicy.decide(coverageSnapshot, claim.requestedAmount());
TransitionDecision transition = transitionPolicy.decide(new ClaimTransitionContext(
claim.status(),
command.requestedDecision(),
authorization,
documents,
fraudRisk,
coverage
));
claim.applyTransition(transition, command.comment(), actor.id());
claimRepository.save(claim);
auditLogPort.append(claim.id(), "CLAIM_STATUS_CHANGED", actor.id().value(), command.correlationId());
ProcessClaimDecisionResult result = ProcessClaimDecisionResult.builder(claim.id())
.previousStatus(claim.previousStatus())
.newStatus(claim.status())
.reason(transition.reason())
.paymentPreparationRequired(claim.status().requiresPaymentPreparation())
.customerNotificationRequired(claim.status().requiresCustomerNotification())
.managerEscalationRequired(claim.status().requiresManagerEscalation())
.emit("CLAIM_DECISION_PROCESSED")
.build();
if (result.paymentPreparationRequired()) {
PaymentPreparationRequest request = PaymentPreparationFactory.from(claim, command);
paymentReleasePort.preparePayment(request);
outbox.enqueuePaymentPrepared(claim.id(), command.correlationId());
}
if (result.customerNotificationRequired()) {
outbox.enqueueCustomerNotification(claim.id(), claim.customerId(), result.reason(), command.correlationId());
}
if (result.managerEscalationRequired()) {
outbox.enqueueManagerEscalation(claim.id(), result.reason(), command.correlationId());
}
return result;
}
}Was ist hier bewusst noch nicht perfekt?
Der Handler ist schon viel besser als die Monster Method, aber noch
relativ breit. In späteren Runden kann daraus ein
ClaimDecisionWorkflowOrchestrator mit einzelnen Schritten
entstehen.
Pattern / Konzept
- Application Service
- Command Handler
- Ports & Adapters
- Policy Objects
- Outbox
Tests
- Handler Test mit Fake Ports
- Golden-Master-Vergleich gegen Legacy
- Authorization Policy Test
- Transition Policy Test
- Payment Idempotency Test
Risiko
Wenn Payment noch synchron ist, bleibt ein technisches Risiko im Use Case. Später kann Payment vollständig über Outbox/Worker entkoppelt werden.
Schritt 14: Workflow Orchestrator einführen
Für sehr komplexe Abläufe wird der Handler schlanker gemacht. Der Ablauf wird in Schritte zerlegt.
Datei:
claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowOrchestrator.javapackage com.seb4u.demo.claims.workflow;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import java.util.List;
public final class ClaimDecisionWorkflowOrchestrator {
private final List<ClaimDecisionWorkflowStep> steps;
public ClaimDecisionWorkflowOrchestrator(List<ClaimDecisionWorkflowStep> steps) {
this.steps = List.copyOf(steps);
}
public ProcessClaimDecisionResult run(ProcessClaimDecisionCommand command, ClaimDecisionWorkflowContext context) {
ClaimDecisionWorkflowContext current = context;
for (ClaimDecisionWorkflowStep step : steps) {
ClaimDecisionStepResult stepResult = step.execute(command, current);
current = stepResult.context();
if (stepResult.stopWorkflow()) {
return stepResult.toProcessResult();
}
}
return current.toProcessResult();
}
}Datei:
claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowStep.javapackage com.seb4u.demo.claims.workflow;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
public interface ClaimDecisionWorkflowStep {
String name();
ClaimDecisionStepResult execute(
ProcessClaimDecisionCommand command,
ClaimDecisionWorkflowContext context
);
}Beispiel-Schritt:
package com.seb4u.demo.claims.workflow.steps;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.shared.security.SecurityContextPort;
import com.seb4u.demo.claims.workflow.ClaimDecisionStepResult;
import com.seb4u.demo.claims.workflow.ClaimDecisionWorkflowContext;
import com.seb4u.demo.claims.workflow.ClaimDecisionWorkflowStep;
public final class AuthorizationWorkflowStep implements ClaimDecisionWorkflowStep {
private final SecurityContextPort securityContextPort;
private final AuthorizationPolicy authorizationPolicy;
public AuthorizationWorkflowStep(
SecurityContextPort securityContextPort,
AuthorizationPolicy authorizationPolicy) {
this.securityContextPort = securityContextPort;
this.authorizationPolicy = authorizationPolicy;
}
@Override
public String name() {
return "authorization";
}
@Override
public ClaimDecisionStepResult execute(
ProcessClaimDecisionCommand command,
ClaimDecisionWorkflowContext context) {
var actor = securityContextPort.currentActor();
AuthorizationDecision decision = authorizationPolicy.mayProcessDecision(
actor,
context.claim().status(),
command.requestedDecision(),
command.forceApproval()
);
ClaimDecisionWorkflowContext updated = context.withActor(actor).withAuthorization(decision);
if (!decision.allowed()) {
return ClaimDecisionStepResult.stop(updated, decision.asDecisionReason());
}
return ClaimDecisionStepResult.continueWith(updated);
}
}Warum wird das gemacht?
Ein Orchestrator hilft, wenn der Ablauf fachlich lang ist, aber jeder Schritt einzeln testbar sein soll.
Pattern / Konzept
- Workflow Orchestrator
- Chain of Responsibility ähnlich, aber kontrollierter
- Step Result
- Context Object
Tests
- Step-Reihenfolge Test
- Stop-on-denied Test
- Context Propagation Test
Risiko
Ein Orchestrator kann selbst wieder zu einer versteckten Workflow Engine werden. Deshalb müssen Schritte klein, benannt und dokumentiert bleiben.
Teil G: Audit, Outbox, Notification und Search
Schritt 15: Audit Trail abstrahieren
Datei:
claim-audit/src/main/java/com/seb4u/demo/claims/audit/AuditLogPort.javapackage com.seb4u.demo.claims.audit;
import com.seb4u.demo.claims.domain.model.ClaimId;
public interface AuditLogPort {
void append(ClaimId claimId, String eventType, String actorId, String correlationId);
}Datei:
claim-audit/src/main/java/com/seb4u/demo/claims/audit/StructuredAuditLogAdapter.javapackage com.seb4u.demo.claims.audit;
import com.seb4u.demo.claims.domain.model.ClaimId;
import java.time.Clock;
import java.time.Instant;
public final class StructuredAuditLogAdapter implements AuditLogPort {
private final AuditEventRepository repository;
private final Clock clock;
public StructuredAuditLogAdapter(AuditEventRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Override
public void append(ClaimId claimId, String eventType, String actorId, String correlationId) {
AuditEvent event = new AuditEvent(
claimId.value(),
eventType,
actorId,
correlationId,
Instant.now(clock)
);
repository.save(event);
}
}Warum wurde es geändert?
Audit ist fachlich wichtig und darf nicht nur ein technisches Log sein. Es muss nachvollziehbar, testbar und später reportbar sein.
Schritt 16: Outbox Pattern einführen
Verlässlicher Ereignisfluss mit Outbox
Die Outbox koppelt das fachliche Ergebnis atomar an ein veröffentlichbares Ereignis und entkoppelt die technische Zustellung.
- Claim und Outbox-Eintrag entstehen in derselben Datenbanktransaktion.
- Der Publisher darf wiederholen; Konsumenten müssen idempotent sein.
- Monitoring unterscheidet fachliche Fehler von Zustellproblemen.
Was war vorher schlecht?
JMS-Nachrichten wurden direkt aus der EJB gesendet. Dadurch entstehen klassische Fehler:
- DB-Status gespeichert, Event nicht gesendet.
- Event gesendet, DB-Rollback passiert.
- Retry erzeugt doppelte Nachrichten.
Was wird geändert?
Events werden zuerst in eine Outbox-Tabelle geschrieben. Ein Worker publiziert sie später.
Datei:
claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxEvent.javapackage com.seb4u.demo.claims.notification.outbox;
import java.time.Instant;
import java.util.UUID;
public final class OutboxEvent {
private final UUID id;
private final String aggregateId;
private final String eventType;
private final String payloadJson;
private final String correlationId;
private final Instant createdAt;
private final int publishAttempts;
public OutboxEvent(
UUID id,
String aggregateId,
String eventType,
String payloadJson,
String correlationId,
Instant createdAt,
int publishAttempts) {
this.id = id;
this.aggregateId = aggregateId;
this.eventType = eventType;
this.payloadJson = payloadJson;
this.correlationId = correlationId;
this.createdAt = createdAt;
this.publishAttempts = publishAttempts;
}
public UUID id() {
return id;
}
public String aggregateId() {
return aggregateId;
}
public String eventType() {
return eventType;
}
public String payloadJson() {
return payloadJson;
}
public String correlationId() {
return correlationId;
}
public Instant createdAt() {
return createdAt;
}
public int publishAttempts() {
return publishAttempts;
}
}Datei:
claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxPublisherWorker.javapackage com.seb4u.demo.claims.notification.outbox;
import java.util.List;
public final class OutboxPublisherWorker {
private final OutboxRepository repository;
private final MessagePublisher publisher;
public OutboxPublisherWorker(OutboxRepository repository, MessagePublisher publisher) {
this.repository = repository;
this.publisher = publisher;
}
public void publishNextBatch() {
List<OutboxEvent> events = repository.lockNextUnpublishedBatch(100);
for (OutboxEvent event : events) {
try {
publisher.publish(event.eventType(), event.payloadJson(), event.correlationId());
repository.markPublished(event.id());
} catch (Exception ex) {
repository.markFailedAttempt(event.id(), ex.getMessage());
}
}
}
}Pattern / Konzept
- Transactional Outbox
- Retry
- Dead Letter
- Idempotent Consumer
Tests
- Outbox Event Persisted Test
- Worker Publishes Locked Batch Test
- Failed Publish Increments Attempt Test
- Duplicate Event Idempotency Test
Schritt 17: Notification als Port
Datei:
claim-notification/src/main/java/com/seb4u/demo/claims/notification/NotificationPort.javapackage com.seb4u.demo.claims.notification;
import com.seb4u.demo.claims.domain.model.ClaimId;
public interface NotificationPort {
void sendClaimStatusNotification(ClaimId claimId, NotificationRecipient recipient, NotificationTemplate template);
}Der wichtige Punkt:
Im Use Case wird nicht direkt E-Mail oder SMS gesendet. Stattdessen wird ein Outbox Event erzeugt, das später ein Notification Adapter verarbeitet.
Schritt 18: Search und Reporting als Read Models
Was war vorher schlecht?
Suche lief über Oracle LIKE Queries direkt aus dem Backoffice.
Was wird geändert?
Ein ClaimSearchPort trennt die Suchdomäne.
Datei:
claim-search/src/main/java/com/seb4u/demo/claims/search/ClaimSearchPort.javapackage com.seb4u.demo.claims.search;
public interface ClaimSearchPort {
ClaimSearchResult search(ClaimSearchQuery query);
}Datei:
claim-search/src/main/java/com/seb4u/demo/claims/search/ClaimSearchQuery.javapackage com.seb4u.demo.claims.search;
import java.time.LocalDate;
import java.util.Set;
public record ClaimSearchQuery(
String freeText,
Set<String> statuses,
String customerId,
String partnerId,
LocalDate createdFrom,
LocalDate createdTo,
int page,
int size
) {
}Migrationnutzen
Heute kann der Adapter weiterhin Oracle nutzen. Später kann er OpenSearch oder Elasticsearch verwenden, ohne dass Backoffice oder Use Case geändert werden.
Teil H: Legacy UI entkoppeln
Schritt 19: Partner Portal vom EJB entkoppeln
Vorher
LegacyPartnerPortalServlet
-> InitialContext.lookup("ejb/LegacyClaimFacadeBean")
-> processClaimDecision(...)Nachher schrittweise
LegacyPartnerPortalServlet
-> PartnerPortalClaimApplicationService
-> ProcessClaimDecisionHandlerSpäter:
Modern Partner UI / BFF
-> claim-rest-api
-> ProcessClaimDecisionHandlerDatei:
legacy-claims-partner-portal/src/main/java/com/seb4u/demo/claims/portal/PartnerPortalClaimApplicationService.javapackage com.seb4u.demo.claims.portal;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.ActorId;
public final class PartnerPortalClaimApplicationService {
private final ProcessClaimDecisionHandler handler;
private final PartnerPortalCommandMapper mapper;
public PartnerPortalClaimApplicationService(
ProcessClaimDecisionHandler handler,
PartnerPortalCommandMapper mapper) {
this.handler = handler;
this.mapper = mapper;
}
public PartnerPortalDecisionResponse submitDecision(PartnerPortalDecisionRequest request) {
ProcessClaimDecisionCommand command = ProcessClaimDecisionCommand.builder()
.claimId(ClaimId.of(request.claimId()))
.requestedDecision(RequestedDecision.fromLegacyCode(request.decision()))
.actorId(ActorId.of(request.partnerUserId()))
.actorRoles(mapper.mapRoles(request.legacyRoles()))
.comment(mapper.mapComment(request.comment()))
.forceApproval(false)
.correlationId(request.correlationId())
.build();
ProcessClaimDecisionResult result = handler.handle(command);
return mapper.toPortalResponse(result);
}
}Pattern / Konzept
- Strangler UI
- Adapter Layer
- Command Mapping
Risiko
Partner Portal hat oft Session- und Rollenlogik. Diese muss langsam entkoppelt werden, sonst entstehen Sicherheitslücken.
Schritt 20: Internal Backoffice entkoppeln
Backoffice bekommt eine eigene Use-Case-Schicht, statt direkt EJB und Stored Procedures aufzurufen.
Datei:
legacy-internal-claims-backoffice/src/main/java/com/seb4u/demo/claims/backoffice/ClaimBackofficeUseCase.javapackage com.seb4u.demo.claims.backoffice;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.search.ClaimSearchPort;
import com.seb4u.demo.claims.search.ClaimSearchQuery;
import com.seb4u.demo.claims.search.ClaimSearchResult;
public final class ClaimBackofficeUseCase {
private final ClaimSearchPort searchPort;
private final ProcessClaimDecisionHandler decisionHandler;
private final BackofficeCommandMapper commandMapper;
public ClaimBackofficeUseCase(
ClaimSearchPort searchPort,
ProcessClaimDecisionHandler decisionHandler,
BackofficeCommandMapper commandMapper) {
this.searchPort = searchPort;
this.decisionHandler = decisionHandler;
this.commandMapper = commandMapper;
}
public ClaimSearchResult searchClaims(BackofficeSearchRequest request) {
ClaimSearchQuery query = commandMapper.toSearchQuery(request);
return searchPort.search(query);
}
public BackofficeDecisionResponse processDecision(BackofficeDecisionRequest request) {
ProcessClaimDecisionCommand command = commandMapper.toDecisionCommand(request);
ProcessClaimDecisionResult result = decisionHandler.handle(command);
return commandMapper.toDecisionResponse(result);
}
}Teil I: Batch, SLA und Eskalation
Schritt 21: SLA/Eskalation aus TimerBean lösen
Was war vorher schlecht?
Die WebSphere TimerBean hat direkt Oracle Queries, Stored Procedures und Notifications ausgelöst.
Was wird geändert?
SLA-Regeln werden fachlich modelliert. Scheduler wird Infrastruktur.
Datei:
claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationPolicy.javapackage com.seb4u.demo.claims.batch.escalation;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import java.time.Duration;
import java.time.Instant;
public final class EscalationPolicy {
public EscalationDecision decide(EscalationCandidate candidate, Instant now) {
if (candidate.status() == ClaimStatus.CLOSED || candidate.status() == ClaimStatus.REJECTED) {
return EscalationDecision.noEscalation("claim already finished");
}
Duration openDuration = Duration.between(candidate.lastStatusChangedAt(), now);
if (candidate.status() == ClaimStatus.WAITING_FOR_DOCUMENTS
&& openDuration.toHours() > 72) {
return EscalationDecision.escalateToPartnerSupport("documents missing for more than 72 hours");
}
if (candidate.status() == ClaimStatus.PENDING_REVIEW
&& openDuration.toHours() > 48) {
return EscalationDecision.escalateToTeamLead("pending review exceeds 48 hours");
}
if (candidate.status() == ClaimStatus.MANUAL_FRAUD_REVIEW
&& openDuration.toHours() > 24) {
return EscalationDecision.escalateToFraudSpecialist("fraud review exceeds 24 hours");
}
return EscalationDecision.noEscalation("SLA still within limits");
}
}Datei:
claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationBatchJob.javapackage com.seb4u.demo.claims.batch.escalation;
import com.seb4u.demo.claims.notification.outbox.OutboxRepository;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
public final class EscalationBatchJob {
private final EscalationCandidateRepository candidateRepository;
private final EscalationPolicy escalationPolicy;
private final OutboxRepository outboxRepository;
private final LeaseLock leaseLock;
private final Clock clock;
public EscalationBatchJob(
EscalationCandidateRepository candidateRepository,
EscalationPolicy escalationPolicy,
OutboxRepository outboxRepository,
LeaseLock leaseLock,
Clock clock) {
this.candidateRepository = candidateRepository;
this.escalationPolicy = escalationPolicy;
this.outboxRepository = outboxRepository;
this.leaseLock = leaseLock;
this.clock = clock;
}
public void run() {
if (!leaseLock.tryAcquire("claim-escalation-job")) {
return;
}
try {
Instant now = Instant.now(clock);
List<EscalationCandidate> candidates = candidateRepository.findOpenCandidates(500);
for (EscalationCandidate candidate : candidates) {
EscalationDecision decision = escalationPolicy.decide(candidate, now);
if (decision.escalationRequired()) {
candidateRepository.markEscalated(candidate.claimId(), decision.targetQueue(), now);
outboxRepository.enqueueEscalation(candidate.claimId().value(), decision.reason());
}
}
} finally {
leaseLock.release("claim-escalation-job");
}
}
}Pattern / Konzept
- Batch Worker
- Lease Lock
- Policy Object
- Outbox Event
- OpenShift CronJob Vorbereitung
Risiko
Mehrere Pod-Instanzen können denselben Batch ausführen. Deshalb ist ein Lease Lock wichtig.
Teil J: Architekturtests
Schritt 22: ArchUnit-Regeln
Ziel: verhindern, dass nach dem Refactoring wieder technische Abhängigkeiten in die Domain zurückwandern.
Datei:
migration-tests/src/test/java/com/seb4u/demo/claims/architecture/ArchitectureBoundaryTest.javapackage com.seb4u.demo.claims.architecture;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class ArchitectureBoundaryTest {
@Test
void domainMustNotDependOnInfrastructureOrJavaEe() {
var classes = new ClassFileImporter()
.importPackages("com.seb4u.demo.claims");
ArchRule rule = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..infrastructure..",
"javax.ejb..",
"javax.jms..",
"javax.servlet..",
"javax.xml.ws..",
"oracle.jdbc.."
);
rule.check(classes);
}
@Test
void applicationMustUsePortsInsteadOfSoapClientsDirectly() {
var classes = new ClassFileImporter()
.importPackages("com.seb4u.demo.claims");
ArchRule rule = noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().haveSimpleNameEndingWith("SoapClient");
rule.check(classes);
}
}Warum wichtig?
Refactoring ist kein einmaliges Ereignis. Architekturregeln verhindern Rückfälle.
Teil K: Golden Master Vergleich Alt gegen Neu
Legacy vs. Modular Handler Vergleichstest
Datei:
migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyVsModularDecisionComparisonTest.javapackage com.seb4u.demo.claims.migration;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class LegacyVsModularDecisionComparisonTest {
static Stream<GoldenMasterCase> goldenMasterCases() {
return GoldenMasterCaseLoader.load("golden-master/claim-decision-cases.yml").stream();
}
@ParameterizedTest(name = "{0}")
@MethodSource("goldenMasterCases")
void modularHandlerKeepsBusinessRelevantLegacyBehaviour(GoldenMasterCase testCase) throws Exception {
LegacyClaimFacadeBean legacy = LegacyFixtureFactory.legacyFacadeFor(testCase);
ProcessClaimDecisionHandler modular = ModularFixtureFactory.handlerFor(testCase);
GoldenMasterSnapshot legacySnapshot = LegacyRunner.run(legacy, testCase);
ProcessClaimDecisionCommand command = testCase.toCommand();
GoldenMasterSnapshot modularSnapshot = ModularRunner.run(modular, command);
assertThat(modularSnapshot.finalStatus()).isEqualTo(legacySnapshot.finalStatus());
assertThat(modularSnapshot.auditEvents()).containsAll(legacySnapshot.auditEvents());
assertThat(modularSnapshot.outboundEvents()).containsAll(legacySnapshot.outboundEvents());
}
}Wichtig
Der neue Code muss nicht jede technische Nebenwirkung identisch machen. Er muss die fachlich relevanten Ergebnisse kompatibel halten.
Beispiel:
- Alte Stored Procedure
SP_UPDATE_CLAIM_STATUSmuss nicht im neuen System existieren. - Aber der Claim-Status, Audit Trail und die fachliche Reaktion müssen stimmen.
Teil L: Refactoring-Matrix dieser Runde
Refactoring-Schritte als Matrix
| ID | Änderung | Vorher | Nachher | Pattern / Konzept | Test / Sicherheitsnetz | Risiko |
|---|---|---|---|---|---|---|
| R3-01 | Characterization Tests | kein Sicherheitsnetz | Golden Master Cases | Characterization Test | Legacy Snapshot Test | schlechtes Verhalten wird konserviert |
| R3-02 | Parameter Object | viele Primitive | LegacyDecisionInput |
Parameter Object | bestehender Legacy Test | Verschieben kann Seiteneffekte ändern |
| R3-03 | Command Object | SOAP/EJB Parameter | ProcessClaimDecisionCommand |
Command Pattern | Mapping Test | zu frühe Typisierung |
| R3-04 | Result Object | String/Exception | ProcessClaimDecisionResult |
Result Object | Response Mapping Test | Legacy Fehlercodes |
| R3-05 | Statuslogik | if/else | ClaimTransitionPolicy |
State/Policy | Transition Tests | Reihenfolge der Regeln |
| R3-06 | Document Port | File Share direkt | DocumentVerificationPort |
Port/Adapter | Adapter Test | OpenShift Storage |
| R3-07 | Fraud Port | SOAP direkt | FraudRiskPort |
ACL | Timeout Tests | technische Fehlerklassifikation |
| R3-08 | Coverage Policy | SOAP/SP gemischt | CoveragePolicy |
Specification | Coverage Tests | alte Tariflogik unvollständig |
| R3-09 | Payment Port | SOAP/JTA direkt | PaymentReleasePort |
Idempotency | Payment Tests | Doppelzahlung |
| R3-10 | Security Policy | JAAS direkt | AuthorizationPolicy |
Policy | Auth Tests | Role Mapping |
| R3-11 | Workflow | EJB Ablauf | Orchestrator | Workflow | Step Tests | zu komplexe Mini-Engine |
| R3-12 | Audit | SP/Logs | AuditLogPort |
Audit Event | Audit Tests | Compliance-Anforderungen |
| R3-13 | Outbox | JMS direkt | Outbox Worker | Outbox | Publish Tests | Eventual Consistency |
| R3-14 | Batch | TimerBean | Batch Worker | Lease Lock | Batch Tests | Parallelität in Pods |
| R3-15 | UI Entkopplung | JSP direkt an EJB | Use Case / REST | Strangler | UI Adapter Tests | Session/Security |
Teil M: Before/After-Mapping-Vorbereitung aus Runde 3
Mapping-Kandidaten
| Legacy-Datei / Modul | Neuer Ziel-Ort | Grund | Pattern / Konzept | Sicherheitsnetz |
|---|---|---|---|---|
LegacyClaimFacadeBean.java |
ProcessClaimDecisionHandler.java |
Monster Method zerlegen | Command Handler | Golden Master + Handler Test |
LegacyClaimStatusIfElse.java |
ClaimTransitionPolicy.java |
Statuslogik isolieren | State/Policy | Transition Test |
LegacyDocumentShareClient.java |
DocumentVerificationPort.java +
FileShareDocumentVerificationAdapter.java |
File Share entkoppeln | Port/Adapter | Adapter Test |
FraudRiskSoapClient.java |
FraudRiskPort.java +
FraudRiskSoapAdapter.java |
SOAP entkoppeln | ACL | Timeout/Fallback Test |
PolicyCoverageSoapClient.java |
CoveragePort.java +
CoveragePolicy.java |
technische und fachliche Coverage trennen | Specification | Coverage Test |
PaymentReleaseSoapClient.java |
PaymentReleasePort.java |
Zahlung absichern | Idempotency | Idempotency Test |
LegacyNotificationSenderBean.java |
NotificationPort.java + Outbox |
Nebenwirkung trennen | Outbox | Outbox Test |
SlaEscalationTimerBean.java |
EscalationBatchJob.java +
EscalationPolicy.java |
Scheduler entkoppeln | Batch Worker | Batch Test |
AuditReportStoredProcedureDao.java |
AuditLogPort.java +
AuditReportingAdapter.java |
Audit sichtbar machen | Audit Event | Audit Test |
LegacyClaimSearchDao.java |
ClaimSearchPort.java |
Suche trennen | Read Model | Search Adapter Test |
CustomerMasterDataSoapClient.java |
CustomerMasterDataPort.java |
Stammdaten entkoppeln | ACL | Contract Test |
JaasRoleChecker.java |
SecurityContextPort.java +
AuthorizationPolicy.java |
AuthN/AuthZ trennen | Policy | Authorization Test |
Teil N: Didaktische Erklärung: Warum nicht alles auf einmal?
Typischer Fehler bei Enterprise-Modernisierung
Ein häufiger Fehler ist:
Wir bauen sofort Microservices, REST, Kubernetes und OpenShift.
Das klingt modern, löst aber das eigentliche Problem nicht. Wenn die Fachlogik weiterhin unklar ist, wird aus einer WebSphere-Monster-Method nur ein Container-Monster-Service.
Besser:
- Legacy-Verhalten einfrieren.
- Fachlogik sichtbar machen.
- technische Abhängigkeiten kapseln.
- fachliche Module schneiden.
- Tests aufbauen.
- Runtime später wechseln.
Welche Teile bleiben zunächst Legacy-kompatibel?
Während des Refactorings bleiben diese Schnittstellen zunächst kompatibel:
- SOAP Operation
processClaimDecision - Partner Portal Servlet URLs
- Backoffice JSP-Flows
- Oracle Tabellen
- teilweise Stored Procedures
- JMS Eventnamen
- LDAP/JAAS Rollen
Warum?
Weil ein Big-Bang-Umbau zu riskant wäre.
Die Legacy-Schnittstellen werden nur zu Adaptern. Dahinter entsteht schrittweise moderne Struktur.
Welche Teile werden zuerst neu?
Priorität für neue Struktur:
- Fachliche Value Objects
- Transition Policy
- Authorization Policy
- Ports für externe Systeme
- Command Handler
- Audit Port
- Outbox
- Workflow Orchestrator
- Batch Worker
- REST API
- OpenShift Deployment
Teil O: Tests nach Refactoring-Ebene
Testpyramide für dieses Projekt
| Ebene | Testtyp | Zweck | Beispiel |
|---|---|---|---|
| Domain | Unit Tests | Fachregeln prüfen | ClaimTransitionPolicyTest |
| Application | Handler Tests | Use Case prüfen | ProcessClaimDecisionHandlerTest |
| Adapter | Adapter Tests | technische Übersetzung prüfen | FraudRiskSoapAdapterTest |
| Contract | SOAP/REST Contract Tests | Schnittstellen stabil halten | ClaimSoapCompatibilityTest |
| Migration | Golden Master Tests | Legacy vs. Neu vergleichen | LegacyVsModularDecisionComparisonTest |
| Architecture | ArchUnit | Grenzen sichern | ArchitectureBoundaryTest |
| Batch | Batch Tests | SLA/Eskalation prüfen | EscalationBatchJobTest |
| OpenShift später | Smoke Tests | Runtime prüfen | ClaimRuntimeSmokeTest |
Handler Test mit Fakes
Datei:
claim-application/src/test/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandlerTest.javapackage com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.application.testing.FakeAuditLogPort;
import com.seb4u.demo.claims.application.testing.FakeClaimRepository;
import com.seb4u.demo.claims.application.testing.FakeCoveragePort;
import com.seb4u.demo.claims.application.testing.FakeCustomerMasterDataPort;
import com.seb4u.demo.claims.application.testing.FakeDocumentVerificationPort;
import com.seb4u.demo.claims.application.testing.FakeFraudRiskPort;
import com.seb4u.demo.claims.application.testing.FakeOutbox;
import com.seb4u.demo.claims.application.testing.FakePaymentReleasePort;
import com.seb4u.demo.claims.application.testing.FakeSecurityContextPort;
import com.seb4u.demo.claims.domain.coverage.CoveragePolicy;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionPolicy;
import com.seb4u.demo.claims.shared.security.ActorId;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ProcessClaimDecisionHandlerTest {
@Test
void approvesCoveredLowRiskClaimAndCreatesPaymentOutboxEvent() {
ClaimId claimId = ClaimId.of("CLM-12345");
FakeClaimRepository claims = new FakeClaimRepository()
.withClaim(Claim.pendingReview(claimId, "CUST-4711", "POL-2024-0001", "1200.00"));
FakeOutbox outbox = new FakeOutbox();
FakePaymentReleasePort payment = new FakePaymentReleasePort();
FakeAuditLogPort audit = new FakeAuditLogPort();
ProcessClaimDecisionHandler handler = new ProcessClaimDecisionHandler(
claims,
FakeCustomerMasterDataPort.standardCustomer(),
FakeSecurityContextPort.manager("manager-1"),
new AuthorizationPolicy(),
FakeDocumentVerificationPort.allVerified(),
FakeFraudRiskPort.lowRisk(12),
FakeCoveragePort.covered(),
new CoveragePolicy(),
new ClaimTransitionPolicy(),
payment,
audit,
outbox
);
ProcessClaimDecisionCommand command = ProcessClaimDecisionCommand.builder()
.claimId(claimId)
.requestedDecision(RequestedDecision.APPROVE)
.actorId(ActorId.of("manager-1"))
.actorRoles(FakeSecurityContextPort.managerRoles())
.comment("approved after review")
.forceApproval(false)
.correlationId("corr-123")
.build();
ProcessClaimDecisionResult result = handler.handle(command);
assertThat(result.newStatus().name()).isEqualTo("PAYMENT_PENDING");
assertThat(payment.preparedPayments()).hasSize(1);
assertThat(outbox.events()).extracting("eventType")
.contains("PAYMENT_PREPARED");
assertThat(audit.events()).extracting("eventType")
.contains("CLAIM_DECISION_STARTED", "CLAIM_STATUS_CHANGED");
}
}Teil P: Risiken nach Runde 3
Noch offene technische Risiken
| Risiko | Beschreibung | Gegenmaßnahme |
|---|---|---|
| Hidden Stored Procedure Logic | alte Stored Procedures enthalten Fachlogik | DB-Inventory + Characterization Tests |
| SOAP Timeout Semantik | unklar, ob Operation ausgeführt wurde | Idempotency + Contract Tests |
| Payment Doppelverarbeitung | Retry kann Zahlung doppelt vorbereiten | Idempotency Key + Payment Status Check |
| JAAS Rollenmapping | moderne Rollen weichen ab | Role Mapping Dokument + Auth Tests |
| File Share auf OpenShift | lokale Pfade funktionieren nicht stabil | Document Port + Object Storage Adapter später |
| Audit Compliance | neues Audit muss regulatorisch reichen | Audit Event Catalog + Compliance Review |
| Eventual Consistency | Outbox entkoppelt Nebenwirkungen | UI-Status klar anzeigen, Retry/Dead Letter |
| Batch Parallelität | mehrere Pods können Job parallel ausführen | Lease Lock |
| Legacy UI Session | JSP-Session kann Auth-Kontext verstecken | Strangler UI Adapter + SecurityContextPort |
Teil Q: Ergebnis dieser Runde
Ergebnis
Runde 3 hat den Refactoring-Lerninhalt aufgebaut:
- Sicherheitsnetz mit Characterization Tests und Golden Master
- Refactoring-Seam für die Monster Method
- Command Object
ProcessClaimDecisionCommand - Result Object
ProcessClaimDecisionResult - Value Objects und fachliche Statusmodellierung
ClaimTransitionPolicy- Ports für Document, Fraud, Coverage, Payment, Customer und Security
AuthorizationPolicyProcessClaimDecisionHandler- Workflow Orchestrator
- Audit Port
- Outbox Pattern
- Notification/Search/Reporting-Trennung
- Batch/SLA-Entkopplung
- Partner Portal und Backoffice Entkopplung
- ArchUnit-Regeln
- Legacy-vs-Modular-Golden-Master-Test
- Mapping-Kandidaten für spätere Before/After-Dokumentation
Erzeugte Kapitel / Module dieser Runde
Inhaltlich erzeugte Kapitel:
- Ziel der Refactoring-Runde
- Refactoring-Grundregel
- Roadmap der Refactoring-Schritte
- Inventory und Characterization
- Golden Master Snapshots
- Refactoring Seam
- Command Object
- Result Object
- Value Objects
- ClaimTransitionPolicy
- DocumentVerificationPort
- FraudRiskPort
- CoveragePort und CoveragePolicy
- PaymentReleasePort und Idempotency
- CustomerMasterDataPort
- SecurityContextPort und AuthorizationPolicy
- ProcessClaimDecisionHandler
- Workflow Orchestrator
- AuditLogPort
- Outbox Pattern
- NotificationPort
- ClaimSearchPort
- Partner Portal Entkopplung
- Backoffice Entkopplung
- EscalationPolicy und BatchJob
- ArchUnit Architekturtests
- Legacy-vs-Modular Vergleichstest
- Refactoring-Matrix
- Before/After-Mapping-Vorbereitung
- Risiken und offene Punkte
Logische Module:
claim-domainclaim-applicationclaim-documentclaim-paymentclaim-auditclaim-notificationclaim-searchclaim-workflowclaim-batchclaim-infrastructuremigration-testslegacy-claims-partner-portallegacy-internal-claims-backoffice
Wichtige Codebeispiele dieser Runde
LegacyProcessClaimDecisionCharacterizationTest.javaGoldenMasterSnapshot.javaLegacyDecisionInput.javaProcessClaimDecisionCommand.javaProcessClaimDecisionResult.javaClaimId.javaRequestedDecision.javaClaimTransitionPolicy.javaClaimTransitionContext.javaTransitionDecision.javaDocumentVerificationPort.javaFileShareDocumentVerificationAdapter.javaFraudRiskPort.javaFraudRiskSoapAdapter.javaCoveragePort.javaCoveragePolicy.javaPaymentReleasePort.javaIdempotencyKey.javaCustomerMasterDataPort.javaSecurityContextPort.javaAuthorizationPolicy.javaProcessClaimDecisionHandler.javaClaimDecisionWorkflowOrchestrator.javaAuthorizationWorkflowStep.javaAuditLogPort.javaStructuredAuditLogAdapter.javaOutboxEvent.javaOutboxPublisherWorker.javaNotificationPort.javaClaimSearchPort.javaPartnerPortalClaimApplicationService.javaClaimBackofficeUseCase.javaEscalationPolicy.javaEscalationBatchJob.javaArchitectureBoundaryTest.javaLegacyVsModularDecisionComparisonTest.javaProcessClaimDecisionHandlerTest.java
Runde-3-Merksätze
- Refactoring beginnt nicht mit schönem Code, sondern mit sicherem Verhalten.
- Eine Monster Method wird nicht durch eine neue Monster-Klasse ersetzt.
- Ports sind nicht nur Interfaces, sondern Migrationsgrenzen.
- Policies machen Fachregeln sichtbar.
- Outbox trennt Fachentscheidung von technischer Zustellung.
- Idempotency schützt vor gefährlichen Wiederholungen.
- Golden Master Tests schützen beim Schneiden, ersetzen aber nicht fachliche Tests.
- OpenShift löst keine fachliche Kopplung. Refactoring muss vorher passieren.
Ende Runde 03.
055. Moderne Zielarchitektur und Maven-Struktur
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Moderne Zielarchitektur und Maven-Struktur in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Große Runde 4: Modernes Zielprojekt mit modularer Maven-Struktur
Diese Runde beschreibt das moderne Zielprojekt nach dem Refactoring. Der Fokus liegt auf einer modularen Maven-Struktur, klaren fachlichen Grenzen, testbarer Fachlogik, kompatiblen Legacy-Boundaries und vorbereiteter Migration nach OpenShift.
Die Runde baut auf den vorherigen Runden auf:
- Runde 1: Zielbild, Systemlandschaft und Gesamtstruktur
- Runde 2: Legacy-Ausgangssystem mit WebSphere/EAR/EJB/WAR, SOAP, JMS, JTA, Oracle und Monster Methods
- Runde 3: Refactoring Schritt für Schritt mit Tests, Commands, Policies, Ports, Orchestrator und Outbox
- Runde 4: modernes Zielprojekt mit Maven-Modulen, Modulgrenzen, Ziel-POMs und Beispielcode
Ziel der modernen Projektstruktur
Das moderne Zielprojekt soll nicht einfach eine technische Kopie des Legacy-Systems sein. Es soll zeigen, wie aus einer schwer migrierbaren Java-EE-Landschaft eine modulare, testbare und containerfähige Architektur entsteht.
Wichtige Ziele:
- Fachlogik unabhängig von WebSphere machen.
- EJB-, SOAP-, JMS-, JDBC-, FileShare- und LDAP-Abhängigkeiten an den Rand verschieben.
- Fachliche Use Cases sichtbar machen.
- Workflow-Regeln testbar machen.
- Legacy-SOAP-Kompatibilität erhalten.
- Moderne REST/OpenAPI-Schicht ergänzen.
- Module für Dokumente, Suche, Audit, Zahlung, Notification, Batch und Security trennen.
- Migration nach OpenShift vorbereiten.
- Tests als Sicherheitsnetz neben die Architektur stellen.
- Zusatzsysteme nicht verstecken, sondern als klare Ports/Adapter modellieren.
Merksatz:
Das Ziel ist nicht „Spring Boot statt WebSphere“, sondern „klare Grenzen statt technischer Verflechtung“.
Moderne Gesamtstruktur
Empfohlene Root-Struktur:
legacy-claims-customer-support-enterprise/
pom.xml
README.md
docs/
architecture/
migration/
refactoring/
testing/
operations/
before_refactoring_legacy/
refactoring_steps/
after_refactoring_modular/
migration_to_openshift/
tests/
modules/
shared-kernel/
claim-domain/
claim-application/
claim-workflow/
claim-document/
claim-search/
claim-notification/
claim-audit/
claim-payment/
claim-infrastructure/
claim-soap-api/
claim-rest-api/
claim-openapi/
claim-batch/
migration-tests/
legacy-websphere-ear/
legacy-claims-partner-portal/
legacy-internal-claims-backoffice/
legacy-document-management-adapter/
legacy-fraud-risk-gateway/
legacy-policy-coverage-system/
legacy-payment-release-system/
legacy-notification-system/
legacy-sla-escalation-scheduler/
legacy-audit-compliance-reporter/
legacy-search-indexer/
legacy-customer-master-data-system/
legacy-identity-role-management/Diese Struktur ist didaktisch absichtlich groß. In einem echten Projekt würde man manche Module später zusammenlegen oder als separate Repositories schneiden. Für das Lernprojekt ist die Sichtbarkeit der Grenzen wichtiger als minimale Dateianzahl.
Architekturprinzipien der Zielstruktur
Modulare Architektur mit fachlichem Zentrum
Die Zielarchitektur schützt Domain und Use Cases vor Protokollen, Datenbanken und Plattformdetails.
- Domain-Objekte kennen keine REST-, SOAP- oder OpenShift-APIs.
- Application Services orchestrieren Use Cases über Ports.
- Adapter bleiben austauschbar und werden separat getestet.
Domain bleibt frei von Infrastruktur
Das Modul claim-domain darf keine Abhängigkeit haben
auf:
- Spring
- Jakarta EE
- JPA
- SOAP
- JMS
- JDBC
- OpenShift
- WebSphere
- Servlet API
- REST Controller
- Oracle Driver
Erlaubt sind:
- reine Java-Klassen
- Value Objects
- Domain Services
- Policies
- fachliche Exceptions
- fachliche Events
Application orchestriert Use Cases
Das Modul claim-application kennt:
- Commands
- Handlers
- Use Cases
- Ports
- Transaktionsgrenzen als abstraktes Konzept
- Idempotency
- Outbox als fachliche Integrationsabsicht
Es kennt nicht direkt:
- SOAP-Client-Implementierungen
- JMS-Templates
- File-System-Pfade
- JNDI-Namen
- konkrete Oracle Stored Procedures
Infrastructure implementiert technische Details
Das Modul claim-infrastructure implementiert Ports:
- SOAP Adapter
- JMS Adapter
- JDBC/JPA Adapter
- FileShare Adapter
- LDAP Adapter
- Audit Adapter
- Search Adapter
- Payment Adapter
- Notification Adapter
API-Module bleiben dünn
claim-soap-api und claim-rest-api sollen
keine Fachlogik enthalten.
Sie tun nur:
- Request entgegennehmen
- DTO validieren
- DTO in Command mappen
- Use Case aufrufen
- Result in Response mappen
- technische Fehler in API-Fehler übersetzen
Legacy-Module bleiben als Lern- und Migrationsanker erhalten
Die Legacy-Module werden nicht gelöscht. Sie bleiben im Projekt sichtbar, damit Before/After-Mapping, Refactoring-Phasen und Migration verständlich bleiben.
Moderne Maven Parent POM
Datei:
pom.xmlBeispiel:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Legacy Claims & Customer Support Enterprise System</name>
<description>Enterprise learning project for refactoring, modernization and OpenShift migration.</description>
<properties>
<java.version>17</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
<assertj.version>3.25.3</assertj.version>
<mockito.version>5.11.0</mockito.version>
<archunit.version>1.2.1</archunit.version>
<jakarta.version>10.0.0</jakarta.version>
<spring.boot.version>3.3.0</spring.boot.version>
<maven.enforcer.version>3.4.1</maven.enforcer.version>
<maven.compiler.version>3.13.0</maven.compiler.version>
<maven.surefire.version>3.2.5</maven.surefire.version>
<maven.failsafe.version>3.2.5</maven.failsafe.version>
<jacoco.version>0.8.12</jacoco.version>
</properties>
<modules>
<module>modules/shared-kernel</module>
<module>modules/claim-domain</module>
<module>modules/claim-application</module>
<module>modules/claim-workflow</module>
<module>modules/claim-document</module>
<module>modules/claim-search</module>
<module>modules/claim-notification</module>
<module>modules/claim-audit</module>
<module>modules/claim-payment</module>
<module>modules/claim-infrastructure</module>
<module>modules/claim-soap-api</module>
<module>modules/claim-rest-api</module>
<module>modules/claim-openapi</module>
<module>modules/claim-batch</module>
<module>modules/migration-tests</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<release>${maven.compiler.release}</release>
<compilerArgs>
<arg>-Xlint:unchecked</arg>
<arg>-Xlint:deprecation</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven.failsafe.version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven.enforcer.version}</version>
<executions>
<execution>
<id>enforce-build-rules</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[17,)</version>
</requireJavaVersion>
<requireMavenVersion>
<version>[3.9,)</version>
</requireMavenVersion>
<banDuplicatePomDependencyVersions/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>Warum Java 17?
- stabiler Enterprise-Standard
- gute OpenShift-/Container-Unterstützung
- moderne Sprachelemente wie Records möglich
- gute Migration von Java 8 aus möglich
- kompatibel mit Spring Boot 3 und Jakarta EE 10
Modul: shared-kernel
Zweck
shared-kernel enthält nur wirklich gemeinsam verwendbare
Basistypen.
Nicht hinein gehören:
- Claim-Fachlogik
- Payment-Fachlogik
- Dokumentenlogik
- große Utility-Sammlungen
- technische Framework-Helfer
Hinein gehören:
- stabile IDs
- Zeitabstraktion
- Fehlerbasistypen
- kleine fachneutrale Value Objects
POM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>shared-kernel</artifactId>
<packaging>jar</packaging>
</project>Code: ClaimId
package com.seb4u.demo.claims.sharedkernel;
import java.util.Objects;
import java.util.UUID;
public final class ClaimId {
private final String value;
private ClaimId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("ClaimId must not be blank");
}
this.value = value;
}
public static ClaimId of(String value) {
return new ClaimId(value);
}
public static ClaimId newId() {
return new ClaimId("CLM-" + UUID.randomUUID());
}
public String value() {
return value;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ClaimId claimId)) {
return false;
}
return Objects.equals(value, claimId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}Code: BusinessClock
package com.seb4u.demo.claims.sharedkernel;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
public final class BusinessClock {
private final Clock clock;
public BusinessClock(Clock clock) {
this.clock = clock;
}
public static BusinessClock systemUtc() {
return new BusinessClock(Clock.systemUTC());
}
public Instant now() {
return Instant.now(clock);
}
public LocalDate today(ZoneId zoneId) {
return LocalDate.now(clock.withZone(zoneId));
}
}Modul: claim-domain
Zweck
claim-domain enthält die eigentliche
Claim-Fachlichkeit:
- Claim
- ClaimStatus
- ClaimDecision
- ClaimTransitionPolicy
- ClaimAmount
- ClaimDocumentReference
- RiskScore
- CoverageDecision
- ClaimDomainEvent
Es kennt keine technischen Adapter.
POM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>claim-domain</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>shared-kernel</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>Code: ClaimStatus
package com.seb4u.demo.claims.domain;
public enum ClaimStatus {
CREATED,
DOCUMENTS_PENDING,
DOCUMENTS_RECEIVED,
DOCUMENTS_VERIFIED,
COVERAGE_CHECKED,
FRAUD_REVIEW_REQUIRED,
MANUAL_REVIEW_REQUIRED,
APPROVAL_REQUIRED,
APPROVED,
REJECTED,
PAYMENT_RELEASE_REQUESTED,
PAYMENT_RELEASED,
ESCALATED,
CLOSED
}Code: ClaimDecisionType
package com.seb4u.demo.claims.domain;
public enum ClaimDecisionType {
APPROVE,
REJECT,
REQUEST_MORE_DOCUMENTS,
ESCALATE,
SEND_TO_MANUAL_REVIEW,
RELEASE_PAYMENT
}Code: ClaimTransitionPolicy
package com.seb4u.demo.claims.domain;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public final class ClaimTransitionPolicy {
private final Map<ClaimStatus, Set<ClaimDecisionType>> allowedDecisions;
public ClaimTransitionPolicy() {
this.allowedDecisions = new EnumMap<>(ClaimStatus.class);
allowedDecisions.put(ClaimStatus.CREATED, EnumSet.of(
ClaimDecisionType.REQUEST_MORE_DOCUMENTS,
ClaimDecisionType.SEND_TO_MANUAL_REVIEW
));
allowedDecisions.put(ClaimStatus.DOCUMENTS_RECEIVED, EnumSet.of(
ClaimDecisionType.APPROVE,
ClaimDecisionType.REJECT,
ClaimDecisionType.ESCALATE,
ClaimDecisionType.SEND_TO_MANUAL_REVIEW
));
allowedDecisions.put(ClaimStatus.DOCUMENTS_VERIFIED, EnumSet.of(
ClaimDecisionType.APPROVE,
ClaimDecisionType.REJECT,
ClaimDecisionType.ESCALATE
));
allowedDecisions.put(ClaimStatus.APPROVED, EnumSet.of(
ClaimDecisionType.RELEASE_PAYMENT
));
}
public void assertDecisionAllowed(ClaimStatus currentStatus, ClaimDecisionType decisionType) {
Set<ClaimDecisionType> decisions = allowedDecisions.getOrDefault(currentStatus, Set.of());
if (!decisions.contains(decisionType)) {
throw new InvalidClaimTransitionException(
"Decision " + decisionType + " is not allowed for current status " + currentStatus
);
}
}
public ClaimStatus nextStatus(ClaimStatus currentStatus, ClaimDecisionType decisionType) {
assertDecisionAllowed(currentStatus, decisionType);
return switch (decisionType) {
case APPROVE -> ClaimStatus.APPROVED;
case REJECT -> ClaimStatus.REJECTED;
case REQUEST_MORE_DOCUMENTS -> ClaimStatus.DOCUMENTS_PENDING;
case ESCALATE -> ClaimStatus.ESCALATED;
case SEND_TO_MANUAL_REVIEW -> ClaimStatus.MANUAL_REVIEW_REQUIRED;
case RELEASE_PAYMENT -> ClaimStatus.PAYMENT_RELEASE_REQUESTED;
};
}
}Code: Claim Aggregate
package com.seb4u.demo.claims.domain;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class Claim {
private final ClaimId id;
private final String customerNumber;
private final BigDecimal requestedAmount;
private ClaimStatus status;
private final List<ClaimDomainEvent> domainEvents = new ArrayList<>();
public Claim(ClaimId id,
String customerNumber,
BigDecimal requestedAmount,
ClaimStatus status) {
this.id = Objects.requireNonNull(id);
this.customerNumber = requireText(customerNumber, "customerNumber");
this.requestedAmount = Objects.requireNonNull(requestedAmount);
this.status = Objects.requireNonNull(status);
}
public ClaimId id() {
return id;
}
public String customerNumber() {
return customerNumber;
}
public BigDecimal requestedAmount() {
return requestedAmount;
}
public ClaimStatus status() {
return status;
}
public void applyDecision(ClaimDecision decision,
ClaimTransitionPolicy transitionPolicy,
Instant decidedAt) {
Objects.requireNonNull(decision);
Objects.requireNonNull(transitionPolicy);
Objects.requireNonNull(decidedAt);
ClaimStatus oldStatus = this.status;
ClaimStatus newStatus = transitionPolicy.nextStatus(oldStatus, decision.type());
this.status = newStatus;
domainEvents.add(new ClaimDecisionApplied(
id,
oldStatus,
newStatus,
decision.type(),
decision.decidedBy(),
decidedAt
));
}
public List<ClaimDomainEvent> pullDomainEvents() {
List<ClaimDomainEvent> copy = List.copyOf(domainEvents);
domainEvents.clear();
return copy;
}
public List<ClaimDomainEvent> peekDomainEvents() {
return Collections.unmodifiableList(domainEvents);
}
private static String requireText(String value, String fieldName) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(fieldName + " must not be blank");
}
return value;
}
}Code: ClaimDecision
package com.seb4u.demo.claims.domain;
public record ClaimDecision(
ClaimDecisionType type,
String reasonCode,
String comment,
String decidedBy
) {
public ClaimDecision {
if (type == null) {
throw new IllegalArgumentException("type must not be null");
}
if (decidedBy == null || decidedBy.isBlank()) {
throw new IllegalArgumentException("decidedBy must not be blank");
}
}
}Modul: claim-application
Zweck
claim-application enthält die Use Cases und Ports.
Typische Inhalte:
ProcessClaimDecisionCommandProcessClaimDecisionHandlerClaimRepositoryFraudRiskPortCoveragePortDocumentVerificationPortPaymentReleasePortAuditLogPortOutboxPortSecurityContextPortAuthorizationPolicy
POM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>claim-application</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>shared-kernel</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>claim-domain</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>Code: ProcessClaimDecisionCommand
package com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.math.BigDecimal;
import java.util.List;
public record ProcessClaimDecisionCommand(
ClaimId claimId,
String customerNumber,
String policyNumber,
ClaimDecisionType decisionType,
BigDecimal requestedAmount,
String reasonCode,
String comment,
String actorUserId,
List<String> documentIds,
String idempotencyKey
) {
public ProcessClaimDecisionCommand {
if (claimId == null) {
throw new IllegalArgumentException("claimId must not be null");
}
if (decisionType == null) {
throw new IllegalArgumentException("decisionType must not be null");
}
if (actorUserId == null || actorUserId.isBlank()) {
throw new IllegalArgumentException("actorUserId must not be blank");
}
documentIds = documentIds == null ? List.of() : List.copyOf(documentIds);
}
}Code: Port ClaimRepository
package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.util.Optional;
public interface ClaimRepository {
Optional<Claim> findById(ClaimId claimId);
void save(Claim claim);
}Code: Ports für Zusatzsysteme
package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.util.List;
public interface DocumentVerificationPort {
DocumentVerificationResult verifyDocuments(ClaimId claimId, List<String> documentIds);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
public interface FraudRiskPort {
FraudRiskResult assessRisk(ClaimId claimId, String customerNumber, String policyNumber);
}package com.seb4u.demo.claims.application.port;
import java.math.BigDecimal;
public interface CoveragePort {
CoverageResult checkCoverage(String policyNumber, BigDecimal requestedAmount);
}package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.math.BigDecimal;
public interface PaymentReleasePort {
PaymentReleaseResult requestPaymentRelease(ClaimId claimId, BigDecimal amount, String idempotencyKey);
}package com.seb4u.demo.claims.application.port;
public interface CustomerMasterDataPort {
CustomerSnapshot loadCustomerSnapshot(String customerNumber);
}package com.seb4u.demo.claims.application.port;
public interface SecurityContextPort {
UserSecurityContext loadUserContext(String actorUserId);
}Code: ProcessClaimDecisionHandler
package com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.application.port.*;
import com.seb4u.demo.claims.domain.*;
import com.seb4u.demo.claims.sharedkernel.BusinessClock;
import java.util.Objects;
public final class ProcessClaimDecisionHandler {
private final ClaimRepository claimRepository;
private final DocumentVerificationPort documentVerificationPort;
private final FraudRiskPort fraudRiskPort;
private final CoveragePort coveragePort;
private final PaymentReleasePort paymentReleasePort;
private final CustomerMasterDataPort customerMasterDataPort;
private final SecurityContextPort securityContextPort;
private final AuditLogPort auditLogPort;
private final OutboxPort outboxPort;
private final ClaimTransitionPolicy transitionPolicy;
private final AuthorizationPolicy authorizationPolicy;
private final BusinessClock clock;
public ProcessClaimDecisionHandler(ClaimRepository claimRepository,
DocumentVerificationPort documentVerificationPort,
FraudRiskPort fraudRiskPort,
CoveragePort coveragePort,
PaymentReleasePort paymentReleasePort,
CustomerMasterDataPort customerMasterDataPort,
SecurityContextPort securityContextPort,
AuditLogPort auditLogPort,
OutboxPort outboxPort,
ClaimTransitionPolicy transitionPolicy,
AuthorizationPolicy authorizationPolicy,
BusinessClock clock) {
this.claimRepository = Objects.requireNonNull(claimRepository);
this.documentVerificationPort = Objects.requireNonNull(documentVerificationPort);
this.fraudRiskPort = Objects.requireNonNull(fraudRiskPort);
this.coveragePort = Objects.requireNonNull(coveragePort);
this.paymentReleasePort = Objects.requireNonNull(paymentReleasePort);
this.customerMasterDataPort = Objects.requireNonNull(customerMasterDataPort);
this.securityContextPort = Objects.requireNonNull(securityContextPort);
this.auditLogPort = Objects.requireNonNull(auditLogPort);
this.outboxPort = Objects.requireNonNull(outboxPort);
this.transitionPolicy = Objects.requireNonNull(transitionPolicy);
this.authorizationPolicy = Objects.requireNonNull(authorizationPolicy);
this.clock = Objects.requireNonNull(clock);
}
public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
Objects.requireNonNull(command);
UserSecurityContext userContext = securityContextPort.loadUserContext(command.actorUserId());
authorizationPolicy.assertCanProcessDecision(userContext, command.decisionType());
Claim claim = claimRepository.findById(command.claimId())
.orElseThrow(() -> new ClaimNotFoundException(command.claimId()));
CustomerSnapshot customer = customerMasterDataPort.loadCustomerSnapshot(command.customerNumber());
DocumentVerificationResult documentResult = documentVerificationPort.verifyDocuments(
command.claimId(),
command.documentIds()
);
CoverageResult coverageResult = coveragePort.checkCoverage(
command.policyNumber(),
command.requestedAmount()
);
FraudRiskResult fraudRiskResult = fraudRiskPort.assessRisk(
command.claimId(),
command.customerNumber(),
command.policyNumber()
);
DecisionPreconditionGuard.assertDecisionCanContinue(
documentResult,
coverageResult,
fraudRiskResult,
customer
);
ClaimDecision decision = new ClaimDecision(
command.decisionType(),
command.reasonCode(),
command.comment(),
command.actorUserId()
);
claim.applyDecision(decision, transitionPolicy, clock.now());
claimRepository.save(claim);
if (command.decisionType() == ClaimDecisionType.RELEASE_PAYMENT) {
paymentReleasePort.requestPaymentRelease(
claim.id(),
claim.requestedAmount(),
command.idempotencyKey()
);
}
auditLogPort.recordClaimDecision(claim, decision, userContext, clock.now());
outboxPort.storeDomainEvents(claim.pullDomainEvents());
return ProcessClaimDecisionResult.success(claim.id(), claim.status());
}
}Was ist daran besser als die Legacy-Methode?
- keine direkte SOAP-Erzeugung im Use Case
- keine direkte JMS-Publizierung
- keine direkte Stored Procedure
- keine harte JAAS-Rollenprüfung im EJB
- keine FileShare-Pfade in der Fachlogik
- Statuslogik in Policy ausgelagert
- Zusatzsysteme über Ports entkoppelt
- Tests können Ports faken
- Migration nach OpenShift wird möglich
Modul: claim-workflow
Zweck
claim-workflow enthält explizite
Workflow-Komponenten:
- Claim Decision Workflow
- Approval Chain
- Escalation Policy
- SLA Rules
- Claim Assignment Rules
Warum eigenes Modul?
Im Legacy-System steckt Workflow häufig verstreut in:
- EJB-Fassade
- TimerBeans
- Stored Procedures
- JSP-Actions
- SOAP-Endpunkten
- Batchjobs
Das Modul macht den Workflow sichtbar.
Code: Workflow Orchestrator
package com.seb4u.demo.claims.workflow;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
import java.util.Objects;
public final class ClaimDecisionWorkflowOrchestrator {
private final ProcessClaimDecisionHandler decisionHandler;
private final ApprovalChain approvalChain;
private final EscalationPolicy escalationPolicy;
private final WorkflowAuditPort workflowAuditPort;
public ClaimDecisionWorkflowOrchestrator(ProcessClaimDecisionHandler decisionHandler,
ApprovalChain approvalChain,
EscalationPolicy escalationPolicy,
WorkflowAuditPort workflowAuditPort) {
this.decisionHandler = Objects.requireNonNull(decisionHandler);
this.approvalChain = Objects.requireNonNull(approvalChain);
this.escalationPolicy = Objects.requireNonNull(escalationPolicy);
this.workflowAuditPort = Objects.requireNonNull(workflowAuditPort);
}
public ProcessClaimDecisionResult process(ProcessClaimDecisionCommand command) {
WorkflowPlan plan = buildWorkflowPlan(command);
workflowAuditPort.recordPlan(command.claimId(), plan);
if (plan.requiresApproval()) {
approvalChain.requestApproval(command, plan);
return ProcessClaimDecisionResult.pendingApproval(command.claimId());
}
if (plan.requiresEscalation()) {
escalationPolicy.escalate(command.claimId(), plan.escalationReason());
return ProcessClaimDecisionResult.escalated(command.claimId(), plan.escalationReason());
}
return decisionHandler.handle(command);
}
private WorkflowPlan buildWorkflowPlan(ProcessClaimDecisionCommand command) {
if (command.decisionType() == ClaimDecisionType.APPROVE
&& command.requestedAmount().longValue() > 10000) {
return WorkflowPlan.requiresApproval("Amount exceeds team approval limit");
}
if (command.decisionType() == ClaimDecisionType.ESCALATE) {
return WorkflowPlan.requiresEscalation("Manual escalation requested by actor");
}
return WorkflowPlan.straightThrough();
}
}Modul: claim-document
Zweck
Dokumentenlogik war im Legacy-System besonders eng gekoppelt:
- File-Share-Pfade im Code
- Dokumentprüfung direkt im EJB
- XML-Metadaten direkt in Workflow-Code
- Archivierung über Batch
- Fehlerbehandlung technisch statt fachlich
Modernes Ziel:
DocumentStoragePortDocumentVerificationPortDocumentArchivePortFileShareDocumentAdapterArchiveDocumentAdapter- später S3/Object Storage möglich
Code: DocumentStoragePort
package com.seb4u.demo.claims.document;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.io.InputStream;
public interface DocumentStoragePort {
StoredDocument store(ClaimId claimId, DocumentUploadRequest request, InputStream content);
RetrievedDocument retrieve(DocumentId documentId);
void markAsArchived(DocumentId documentId, ArchiveReference archiveReference);
}Code: FileShareDocumentAdapter
package com.seb4u.demo.claims.infrastructure.document;
import com.seb4u.demo.claims.document.*;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Objects;
public final class FileShareDocumentAdapter implements DocumentStoragePort {
private final Path rootPath;
private final DocumentMetadataRepository metadataRepository;
public FileShareDocumentAdapter(Path rootPath,
DocumentMetadataRepository metadataRepository) {
this.rootPath = Objects.requireNonNull(rootPath);
this.metadataRepository = Objects.requireNonNull(metadataRepository);
}
@Override
public StoredDocument store(ClaimId claimId,
DocumentUploadRequest request,
InputStream content) {
try {
DocumentId documentId = DocumentId.newId();
Path claimFolder = rootPath.resolve(claimId.value());
Files.createDirectories(claimFolder);
Path target = claimFolder.resolve(documentId.value() + "-" + sanitize(request.originalFileName()));
Files.copy(content, target);
StoredDocument document = new StoredDocument(
documentId,
claimId,
request.originalFileName(),
target.toString(),
Instant.now()
);
metadataRepository.save(document);
return document;
} catch (IOException ex) {
throw new DocumentStorageException("Could not store document for claim " + claimId, ex);
}
}
@Override
public RetrievedDocument retrieve(DocumentId documentId) {
StoredDocument metadata = metadataRepository.findById(documentId)
.orElseThrow(() -> new DocumentNotFoundException(documentId));
try {
return new RetrievedDocument(metadata, Files.newInputStream(Path.of(metadata.storagePath())));
} catch (IOException ex) {
throw new DocumentStorageException("Could not retrieve document " + documentId, ex);
}
}
@Override
public void markAsArchived(DocumentId documentId, ArchiveReference archiveReference) {
metadataRepository.markAsArchived(documentId, archiveReference);
}
private String sanitize(String originalFileName) {
return originalFileName.replaceAll("[^a-zA-Z0-9._-]", "_");
}
}Modul: claim-payment
Zweck
Zahlungslogik wird aus dem Claim-Use-Case entkoppelt.
Legacy-Probleme:
- direkte SOAP-Aufrufe aus EJB
- JTA versucht lokale DB und externe Zahlung als eine Einheit zu behandeln
- fehlende Idempotenz
- unklare Kompensation
- Fehler werden mehrfach verarbeitet
Modernes Ziel:
PaymentReleasePortPaymentReleaseSoapAdapter- Idempotency Key
- Outbox Event
- Compensation-Strategie
Code: PaymentReleaseAdapter
package com.seb4u.demo.claims.infrastructure.payment;
import com.seb4u.demo.claims.application.port.PaymentReleasePort;
import com.seb4u.demo.claims.application.port.PaymentReleaseResult;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.math.BigDecimal;
import java.util.Objects;
public final class PaymentReleaseSoapAdapter implements PaymentReleasePort {
private final PaymentReleaseSoapClient soapClient;
private final PaymentIdempotencyRepository idempotencyRepository;
public PaymentReleaseSoapAdapter(PaymentReleaseSoapClient soapClient,
PaymentIdempotencyRepository idempotencyRepository) {
this.soapClient = Objects.requireNonNull(soapClient);
this.idempotencyRepository = Objects.requireNonNull(idempotencyRepository);
}
@Override
public PaymentReleaseResult requestPaymentRelease(ClaimId claimId,
BigDecimal amount,
String idempotencyKey) {
return idempotencyRepository.findExistingResult(idempotencyKey)
.orElseGet(() -> requestAndStoreResult(claimId, amount, idempotencyKey));
}
private PaymentReleaseResult requestAndStoreResult(ClaimId claimId,
BigDecimal amount,
String idempotencyKey) {
PaymentReleaseSoapRequest request = new PaymentReleaseSoapRequest(
claimId.value(),
amount,
idempotencyKey
);
PaymentReleaseSoapResponse response = soapClient.releasePayment(request);
PaymentReleaseResult result = new PaymentReleaseResult(
response.paymentReference(),
response.status(),
response.message()
);
idempotencyRepository.store(idempotencyKey, result);
return result;
}
}Modul: claim-audit
Zweck
Audit ist nicht nur Logging.
Audit muss beantworten:
- Wer hat was entschieden?
- Wann wurde entschieden?
- Mit welcher Rolle?
- Auf Basis welcher Dokumente?
- Welche Systeme wurden gefragt?
- Welche Regel hat entschieden?
- Welche Version der Workflow-Regel war aktiv?
Code: AuditLogPort
package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.domain.ClaimDecision;
import java.time.Instant;
public interface AuditLogPort {
void recordClaimDecision(Claim claim,
ClaimDecision decision,
UserSecurityContext userContext,
Instant recordedAt);
}Code: StructuredAuditLogAdapter
package com.seb4u.demo.claims.infrastructure.audit;
import com.seb4u.demo.claims.application.port.AuditLogPort;
import com.seb4u.demo.claims.application.port.UserSecurityContext;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.domain.ClaimDecision;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
public final class StructuredAuditLogAdapter implements AuditLogPort {
private final AuditEventRepository auditEventRepository;
public StructuredAuditLogAdapter(AuditEventRepository auditEventRepository) {
this.auditEventRepository = Objects.requireNonNull(auditEventRepository);
}
@Override
public void recordClaimDecision(Claim claim,
ClaimDecision decision,
UserSecurityContext userContext,
Instant recordedAt) {
AuditEventEntity event = new AuditEventEntity(
"CLAIM_DECISION_RECORDED",
claim.id().value(),
userContext.userId(),
userContext.roles(),
recordedAt,
Map.of(
"decisionType", decision.type().name(),
"reasonCode", decision.reasonCode(),
"claimStatus", claim.status().name()
)
);
auditEventRepository.save(event);
}
}Modul: claim-search
Zweck
Suche wird aus Oracle-LIKE-Queries herausgelöst.
Legacy:
SELECT * FROM CLAIMS
WHERE UPPER(CUSTOMER_NAME) LIKE UPPER('%'||?||'%')
OR UPPER(CLAIM_NR) LIKE UPPER('%'||?||'%')Modern:
ClaimSearchPortClaimSearchCriteriaClaimSearchResultSearchReadModel- später OpenSearch/Elasticsearch möglich
Code: ClaimSearchPort
package com.seb4u.demo.claims.search;
public interface ClaimSearchPort {
ClaimSearchPage search(ClaimSearchCriteria criteria);
}Code: OracleClaimSearchAdapter
package com.seb4u.demo.claims.infrastructure.search;
import com.seb4u.demo.claims.search.*;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class OracleClaimSearchAdapter implements ClaimSearchPort {
private final DataSource dataSource;
public OracleClaimSearchAdapter(DataSource dataSource) {
this.dataSource = Objects.requireNonNull(dataSource);
}
@Override
public ClaimSearchPage search(ClaimSearchCriteria criteria) {
String sql = """
SELECT CLAIM_ID, CLAIM_NUMBER, CUSTOMER_NUMBER, STATUS, UPDATED_AT
FROM CLAIM_SEARCH_VIEW
WHERE (? IS NULL OR CUSTOMER_NUMBER = ?)
AND (? IS NULL OR STATUS = ?)
AND (? IS NULL OR UPPER(CLAIM_NUMBER) LIKE UPPER(?))
ORDER BY UPDATED_AT DESC
FETCH FIRST ? ROWS ONLY
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, criteria.customerNumber());
statement.setString(2, criteria.customerNumber());
statement.setString(3, criteria.status());
statement.setString(4, criteria.status());
statement.setString(5, criteria.freeText());
statement.setString(6, criteria.freeText() == null ? null : "%" + criteria.freeText() + "%");
statement.setInt(7, criteria.limit());
try (ResultSet rs = statement.executeQuery()) {
List<ClaimSearchHit> hits = new ArrayList<>();
while (rs.next()) {
hits.add(new ClaimSearchHit(
rs.getString("CLAIM_ID"),
rs.getString("CLAIM_NUMBER"),
rs.getString("CUSTOMER_NUMBER"),
rs.getString("STATUS"),
rs.getTimestamp("UPDATED_AT").toInstant()
));
}
return new ClaimSearchPage(hits, hits.size());
}
} catch (Exception ex) {
throw new ClaimSearchException("Could not search claims", ex);
}
}
}Modul: claim-notification
Zweck
Benachrichtigung wird aus EJB und JMS-Code getrennt.
Ziel:
- Template Policy
- NotificationPort
- Outbox Event
- Retry
- Dead Letter
- klare Empfängerlogik
Code: NotificationPort
package com.seb4u.demo.claims.notification;
public interface NotificationPort {
void send(NotificationMessage message);
}Code: NotificationTemplatePolicy
package com.seb4u.demo.claims.notification;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
public final class NotificationTemplatePolicy {
public NotificationTemplate selectTemplate(ClaimDecisionType decisionType) {
return switch (decisionType) {
case APPROVE -> NotificationTemplate.of("claim-approved");
case REJECT -> NotificationTemplate.of("claim-rejected");
case REQUEST_MORE_DOCUMENTS -> NotificationTemplate.of("claim-documents-requested");
case ESCALATE -> NotificationTemplate.of("claim-escalated");
case SEND_TO_MANUAL_REVIEW -> NotificationTemplate.of("claim-manual-review");
case RELEASE_PAYMENT -> NotificationTemplate.of("claim-payment-release");
};
}
}Modul: claim-rest-api
Zweck
claim-rest-api bietet moderne REST-Endpunkte.
Wichtig:
REST ersetzt SOAP nicht sofort. In einer echten Migration bleiben SOAP und REST oft parallel bestehen.
REST-Ziele:
- neue Clients anbinden
- BFF ermöglichen
- Partner Portal entkoppeln
- Backoffice modernisieren
- OpenAPI-Dokumentation erzeugen
POM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>claim-rest-api</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>claim-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>Code: ClaimDecisionRestController
package com.seb4u.demo.claims.rest;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
@RestController
@RequestMapping("/api/claims")
public class ClaimDecisionRestController {
private final ProcessClaimDecisionHandler handler;
public ClaimDecisionRestController(ProcessClaimDecisionHandler handler) {
this.handler = handler;
}
@PostMapping("/{claimId}/decisions")
public ResponseEntity<ClaimDecisionResponse> processDecision(
@PathVariable String claimId,
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
@Valid @RequestBody ClaimDecisionRequest request) {
ProcessClaimDecisionCommand command = new ProcessClaimDecisionCommand(
ClaimId.of(claimId),
request.customerNumber(),
request.policyNumber(),
request.decisionType(),
request.requestedAmount(),
request.reasonCode(),
request.comment(),
request.actorUserId(),
request.documentIds(),
idempotencyKey
);
ProcessClaimDecisionResult result = handler.handle(command);
ClaimDecisionResponse response = ClaimDecisionResponse.from(result);
return ResponseEntity.created(URI.create("/api/claims/" + claimId + "/decisions/latest"))
.body(response);
}
}Code: ClaimDecisionRequest
package com.seb4u.demo.claims.rest;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
import java.util.List;
public record ClaimDecisionRequest(
@NotBlank String customerNumber,
@NotBlank String policyNumber,
@NotNull ClaimDecisionType decisionType,
@Positive BigDecimal requestedAmount,
String reasonCode,
String comment,
@NotBlank String actorUserId,
List<String> documentIds
) {
}Modul: claim-soap-api
Zweck
claim-soap-api hält alte SOAP-Clients kompatibel.
Wichtig:
SOAP bleibt als Boundary bestehen, aber die Fachlogik ist nicht mehr in SOAP/EJB versteckt.
Beispiel: SOAP Boundary ruft modernen Use Case
package com.seb4u.demo.claims.soap;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebService;
@WebService(serviceName = "ClaimDecisionService")
public class ClaimDecisionSoapEndpoint {
private final ProcessClaimDecisionHandler handler;
public ClaimDecisionSoapEndpoint(ProcessClaimDecisionHandler handler) {
this.handler = handler;
}
@WebMethod(operationName = "processClaimDecision")
public ProcessClaimDecisionSoapResponse processClaimDecision(
@WebParam(name = "request") ProcessClaimDecisionSoapRequest request) {
ProcessClaimDecisionCommand command = new ProcessClaimDecisionCommand(
ClaimId.of(request.getClaimId()),
request.getCustomerNumber(),
request.getPolicyNumber(),
ClaimDecisionType.valueOf(request.getDecisionType()),
request.getRequestedAmount(),
request.getReasonCode(),
request.getComment(),
request.getActorUserId(),
request.getDocumentIds(),
request.getIdempotencyKey()
);
ProcessClaimDecisionResult result = handler.handle(command);
return ProcessClaimDecisionSoapResponse.from(result);
}
}Migrationseffekt:
- SOAP-Vertrag kann stabil bleiben.
- Fachlogik ist trotzdem modernisiert.
- Partner müssen nicht sofort migrieren.
- REST kann parallel wachsen.
Modul: claim-openapi
Zweck
claim-openapi enthält OpenAPI-Spezifikationen oder
Generator-Konfiguration.
Beispielstruktur:
claim-openapi/
pom.xml
src/main/resources/openapi/claims-api.yaml
src/main/resources/openapi/partner-portal-api.yaml
src/main/resources/openapi/backoffice-api.yamlBeispiel: claims-api.yaml
openapi: 3.0.3
info:
title: Claims API
version: 1.0.0
description: Modern REST API for claim decision workflows.
paths:
/api/claims/{claimId}/decisions:
post:
summary: Process claim decision
operationId: processClaimDecision
parameters:
- name: claimId
in: path
required: true
schema:
type: string
- name: Idempotency-Key
in: header
required: false
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ClaimDecisionRequest'
responses:
'201':
description: Decision accepted
content:
application/json:
schema:
$ref: '#/components/schemas/ClaimDecisionResponse'
components:
schemas:
ClaimDecisionRequest:
type: object
required:
- customerNumber
- policyNumber
- decisionType
- requestedAmount
- actorUserId
properties:
customerNumber:
type: string
policyNumber:
type: string
decisionType:
type: string
enum:
- APPROVE
- REJECT
- REQUEST_MORE_DOCUMENTS
- ESCALATE
- SEND_TO_MANUAL_REVIEW
- RELEASE_PAYMENT
requestedAmount:
type: number
reasonCode:
type: string
comment:
type: string
actorUserId:
type: string
documentIds:
type: array
items:
type: string
ClaimDecisionResponse:
type: object
properties:
claimId:
type: string
status:
type: string
message:
type: stringModul: claim-batch
Zweck
claim-batch ersetzt TimerBeans und Scheduler-Logik.
Legacy:
@Scheduleoder WebSphere Scheduler- SQL direkt im TimerBean
- Eskalationsregeln hart kodiert
- keine klare Wiederanlaufstrategie
- parallele Batchläufe gefährlich
Modern:
- Batch Worker
- Lease Lock
- EscalationPolicy
- OpenShift CronJob vorbereitet
Code: EscalationBatchJob
package com.seb4u.demo.claims.batch;
import com.seb4u.demo.claims.workflow.EscalationPolicy;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
public final class EscalationBatchJob {
private final BatchLeaseRepository leaseRepository;
private final OpenClaimRepository openClaimRepository;
private final EscalationPolicy escalationPolicy;
private final BatchAuditPort batchAuditPort;
public EscalationBatchJob(BatchLeaseRepository leaseRepository,
OpenClaimRepository openClaimRepository,
EscalationPolicy escalationPolicy,
BatchAuditPort batchAuditPort) {
this.leaseRepository = Objects.requireNonNull(leaseRepository);
this.openClaimRepository = Objects.requireNonNull(openClaimRepository);
this.escalationPolicy = Objects.requireNonNull(escalationPolicy);
this.batchAuditPort = Objects.requireNonNull(batchAuditPort);
}
public void run(Instant now) {
if (!leaseRepository.tryAcquire("claim-escalation-job", now)) {
batchAuditPort.recordSkipped("claim-escalation-job", "Lease already held", now);
return;
}
try {
List<OpenClaimProjection> claims = openClaimRepository.findOpenClaimsForEscalation(now);
for (OpenClaimProjection claim : claims) {
escalationPolicy.evaluateAndEscalate(claim, now);
}
batchAuditPort.recordSuccess("claim-escalation-job", claims.size(), now);
} catch (Exception ex) {
batchAuditPort.recordFailure("claim-escalation-job", ex, now);
throw ex;
} finally {
leaseRepository.release("claim-escalation-job");
}
}
}Modul: claim-infrastructure
Zweck
claim-infrastructure enthält technische
Implementierungen.
Beispiele:
claim-infrastructure/
src/main/java/com/seb4u/demo/claims/infrastructure/
persistence/
JpaClaimRepository.java
ClaimEntity.java
ClaimMapper.java
document/
FileShareDocumentAdapter.java
ArchiveDocumentAdapter.java
fraud/
FraudRiskSoapAdapter.java
coverage/
CoverageSoapAdapter.java
payment/
PaymentReleaseSoapAdapter.java
notification/
JmsNotificationAdapter.java
audit/
StructuredAuditLogAdapter.java
search/
OracleClaimSearchAdapter.java
security/
LdapSecurityContextAdapter.java
outbox/
JdbcOutboxPort.java
OutboxPublisherWorker.java
config/
ClaimsRuntimeConfiguration.javaPOM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>legacy-claims-customer-support-enterprise</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>claim-infrastructure</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>claim-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.seb4u.demo.claims</groupId>
<artifactId>claim-document</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
</project>Code: JpaClaimRepository
package com.seb4u.demo.claims.infrastructure.persistence;
import com.seb4u.demo.claims.application.port.ClaimRepository;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import java.util.Optional;
public final class JpaClaimRepository implements ClaimRepository {
private final SpringDataClaimEntityRepository repository;
private final ClaimEntityMapper mapper;
public JpaClaimRepository(SpringDataClaimEntityRepository repository,
ClaimEntityMapper mapper) {
this.repository = repository;
this.mapper = mapper;
}
@Override
public Optional<Claim> findById(ClaimId claimId) {
return repository.findById(claimId.value()).map(mapper::toDomain);
}
@Override
public void save(Claim claim) {
ClaimEntity entity = mapper.toEntity(claim);
repository.save(entity);
}
}Moderne Konfiguration statt JNDI-Wildwuchs
Legacy:
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("jdbc/ClaimsDS");
String path = System.getProperty("claims.document.share");Modern:
package com.seb4u.demo.claims.infrastructure.config;
import java.nio.file.Path;
public record ClaimsRuntimeConfiguration(
String databaseUrl,
String databaseUser,
String fraudRiskEndpoint,
String coverageEndpoint,
String paymentEndpoint,
String notificationQueueName,
Path documentRootPath,
int soapTimeoutMillis,
int retryAttempts
) {
}In OpenShift kann diese Konfiguration später über:
- ConfigMap
- Secret
- Environment Variables
- Mounted Volumes
- Service DNS
bereitgestellt werden.
Zusatzsysteme als moderne Adapter-Landschaft
| Zusatzsystem | Modernes Port | Adapter | Modul |
|---|---|---|---|
| Legacy Claims Partner Portal | PartnerPortalUseCase | PartnerPortalRestController / BFF Adapter | claim-rest-api |
| Internal Claims Backoffice | ClaimBackofficeUseCase | BackofficeRestController | claim-rest-api |
| Document Management Adapter | DocumentStoragePort | FileShareDocumentAdapter | claim-document / infrastructure |
| Fraud Risk Gateway | FraudRiskPort | FraudRiskSoapAdapter | claim-infrastructure |
| Policy Coverage System | CoveragePort | CoverageSoapAdapter | claim-infrastructure |
| Payment Release System | PaymentReleasePort | PaymentReleaseSoapAdapter | claim-payment / infrastructure |
| Notification System | NotificationPort | JmsNotificationAdapter | claim-notification / infrastructure |
| SLA & Escalation Scheduler | EscalationPolicy | EscalationBatchJob | claim-batch |
| Audit & Compliance Reporter | AuditLogPort | StructuredAuditLogAdapter | claim-audit |
| Search Indexer | ClaimSearchPort | OracleClaimSearchAdapter / SearchIndexAdapter | claim-search |
| Customer Master Data System | CustomerMasterDataPort | CustomerMasterDataSoapAdapter | claim-infrastructure |
| Identity & Role Management | SecurityContextPort | LdapSecurityContextAdapter | claim-infrastructure |
Moderne Security Boundary
Code: SecurityContextPort
package com.seb4u.demo.claims.application.port;
public interface SecurityContextPort {
UserSecurityContext loadUserContext(String userId);
}Code: AuthorizationPolicy
package com.seb4u.demo.claims.application.port;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
public final class AuthorizationPolicy {
public void assertCanProcessDecision(UserSecurityContext context,
ClaimDecisionType decisionType) {
if (decisionType == ClaimDecisionType.APPROVE && !context.hasRole("CLAIMS_APPROVER")) {
throw new AuthorizationException("User is not allowed to approve claims");
}
if (decisionType == ClaimDecisionType.RELEASE_PAYMENT && !context.hasRole("PAYMENT_RELEASE_MANAGER")) {
throw new AuthorizationException("User is not allowed to release payments");
}
if (decisionType == ClaimDecisionType.REJECT && !context.hasAnyRole("CLAIMS_AGENT", "CLAIMS_APPROVER")) {
throw new AuthorizationException("User is not allowed to reject claims");
}
}
}Code: LdapSecurityContextAdapter
package com.seb4u.demo.claims.infrastructure.security;
import com.seb4u.demo.claims.application.port.SecurityContextPort;
import com.seb4u.demo.claims.application.port.UserSecurityContext;
import java.util.Set;
public final class LdapSecurityContextAdapter implements SecurityContextPort {
private final LdapGroupClient ldapGroupClient;
private final RoleMapping roleMapping;
public LdapSecurityContextAdapter(LdapGroupClient ldapGroupClient,
RoleMapping roleMapping) {
this.ldapGroupClient = ldapGroupClient;
this.roleMapping = roleMapping;
}
@Override
public UserSecurityContext loadUserContext(String userId) {
Set<String> ldapGroups = ldapGroupClient.loadGroups(userId);
Set<String> roles = roleMapping.mapGroupsToRoles(ldapGroups);
return new UserSecurityContext(userId, roles);
}
}Outbox-Modul in Infrastructure
Warum Outbox?
Legacy-Problem:
- Claim wird in Oracle geändert.
- JMS Event wird gesendet.
- Payment SOAP wird aufgerufen.
- Notification wird publiziert.
- Irgendwo tritt ein Fehler auf.
- Der fachliche Zustand und die Integration sind nicht mehr konsistent.
Modern:
- Claim wird gespeichert.
- Outbox Event wird in derselben DB-Transaktion gespeichert.
- Worker publiziert Event später zuverlässig.
- Retry und Dead Letter sind möglich.
Code: JdbcOutboxPort
package com.seb4u.demo.claims.infrastructure.outbox;
import com.seb4u.demo.claims.application.port.OutboxPort;
import com.seb4u.demo.claims.domain.ClaimDomainEvent;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.UUID;
public final class JdbcOutboxPort implements OutboxPort {
private final DataSource dataSource;
private final DomainEventJsonMapper mapper;
public JdbcOutboxPort(DataSource dataSource, DomainEventJsonMapper mapper) {
this.dataSource = dataSource;
this.mapper = mapper;
}
@Override
public void storeDomainEvents(List<ClaimDomainEvent> events) {
String sql = """
INSERT INTO OUTBOX_EVENT
(EVENT_ID, EVENT_TYPE, AGGREGATE_ID, PAYLOAD_JSON, STATUS, CREATED_AT)
VALUES (?, ?, ?, ?, 'NEW', SYSTIMESTAMP)
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
for (ClaimDomainEvent event : events) {
statement.setString(1, UUID.randomUUID().toString());
statement.setString(2, event.eventType());
statement.setString(3, event.aggregateId());
statement.setString(4, mapper.toJson(event));
statement.addBatch();
}
statement.executeBatch();
} catch (Exception ex) {
throw new OutboxStorageException("Could not store outbox events", ex);
}
}
}Tests im modernen Zielprojekt
Testpyramide
Migration Tests
^
API Contract Tests
^
Adapter Tests
^
Application Use Case Tests
^
Domain TestsDomain Test
package com.seb4u.demo.claims.domain;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ClaimTransitionPolicyTest {
private final ClaimTransitionPolicy policy = new ClaimTransitionPolicy();
@Test
void allowsApprovalForDocumentsReceived() {
ClaimStatus next = policy.nextStatus(ClaimStatus.DOCUMENTS_RECEIVED, ClaimDecisionType.APPROVE);
assertThat(next).isEqualTo(ClaimStatus.APPROVED);
}
@Test
void rejectsPaymentReleaseForCreatedClaim() {
assertThatThrownBy(() -> policy.nextStatus(ClaimStatus.CREATED, ClaimDecisionType.RELEASE_PAYMENT))
.isInstanceOf(InvalidClaimTransitionException.class);
}
}Application Test mit Fake Ports
package com.seb4u.demo.claims.application.decision;
import com.seb4u.demo.claims.application.port.*;
import com.seb4u.demo.claims.domain.*;
import com.seb4u.demo.claims.sharedkernel.BusinessClock;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
class ProcessClaimDecisionHandlerTest {
@Test
void approvesClaimWhenDocumentsCoverageFraudAndAuthorizationAreValid() {
ClaimId claimId = ClaimId.of("CLM-1001");
InMemoryClaimRepository repository = new InMemoryClaimRepository();
repository.save(new Claim(claimId, "C-77", BigDecimal.valueOf(900), ClaimStatus.DOCUMENTS_RECEIVED));
ProcessClaimDecisionHandler handler = new ProcessClaimDecisionHandler(
repository,
(id, docs) -> DocumentVerificationResult.valid(),
(id, customer, policy) -> FraudRiskResult.lowRisk(),
(policy, amount) -> CoverageResult.covered(),
(id, amount, key) -> PaymentReleaseResult.notRequested(),
customer -> CustomerSnapshot.active(customer),
user -> new UserSecurityContext(user, Set.of("CLAIMS_APPROVER")),
new SpyAuditLogPort(),
new InMemoryOutboxPort(),
new ClaimTransitionPolicy(),
new AuthorizationPolicy(),
new BusinessClock(Clock.fixed(Instant.parse("2026-07-05T08:00:00Z"), ZoneOffset.UTC))
);
ProcessClaimDecisionResult result = handler.handle(new ProcessClaimDecisionCommand(
claimId,
"C-77",
"P-300",
ClaimDecisionType.APPROVE,
BigDecimal.valueOf(900),
"OK",
"All documents valid",
"agent-1",
List.of("DOC-1"),
"idem-1"
));
assertThat(result.status()).isEqualTo(ClaimStatus.APPROVED);
assertThat(repository.findById(claimId)).get().extracting(Claim::status).isEqualTo(ClaimStatus.APPROVED);
}
static final class InMemoryClaimRepository implements ClaimRepository {
private final Map<ClaimId, Claim> claims = new HashMap<>();
@Override
public Optional<Claim> findById(ClaimId claimId) {
return Optional.ofNullable(claims.get(claimId));
}
@Override
public void save(Claim claim) {
claims.put(claim.id(), claim);
}
}
}Hinweis: In der finalen Beispielprojekt-Datei werden fehlende Imports ergänzt. Im Lerntext ist die Struktur wichtiger als Kompilierbarkeit jedes Ausschnitts.
Architekturtests mit ArchUnit
Ziel
Architekturtests verhindern, dass später wieder Fachlogik in Adapter oder APIs wandert.
Code
package com.seb4u.demo.claims.architecture;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class ArchitectureBoundaryTest {
@Test
void domainMustNotDependOnFrameworks() {
var classes = new ClassFileImporter().importPackages("com.seb4u.demo.claims");
ArchRule rule = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..",
"jakarta.persistence..",
"jakarta.jms..",
"jakarta.jws..",
"javax.sql..",
"java.sql.."
);
rule.check(classes);
}
@Test
void applicationMustNotDependOnInfrastructure() {
var classes = new ClassFileImporter().importPackages("com.seb4u.demo.claims");
ArchRule rule = noClasses()
.that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..");
rule.check(classes);
}
}Moderne Runtime-Varianten
Das Lernprojekt erlaubt mehrere Ziel-Runtimes:
Open Liberty / Jakarta
Geeignet, wenn:
- SOAP/Jakarta-Nähe wichtig bleibt
- bestehendes Java-EE-Wissen weiterverwendet wird
- Migration von WebSphere zu Open Liberty bevorzugt wird
Spring Boot
Geeignet, wenn:
- REST/OpenAPI im Vordergrund steht
- moderne Cloud-Native-Patterns einfach eingebaut werden sollen
- Team bereits Spring nutzt
Hybrid
Realistisch bei großen Migrationen:
- SOAP-kompatible Boundary eventuell Jakarta/Open Liberty
- neue REST/BFF APIs eventuell Spring Boot
- Domain/Application bleibt frameworkarm
Merksatz:
Die wichtigste Entscheidung ist nicht Spring oder Jakarta. Die wichtigste Entscheidung ist, dass Domain und Application nicht von beiden abhängig werden.
Moderne Deployment-Struktur vorbereitet für OpenShift
Noch keine vollständige OpenShift-Runde, aber die Zielstruktur wird vorbereitet:
migration_to_openshift/
base/
deployment.yaml
service.yaml
route.yaml
configmap.yaml
secret-template.yaml
overlays/
dev/
test/
prod/
cronjobs/
claim-escalation-cronjob.yaml
docs/
runtime-boundaries.md
configuration-strategy.md
secret-strategy.mdDiese Dateien werden in Runde 5 ausführlich erzeugt.
Before/After-Mapping aus Runde 4
| Legacy-Datei | Moderne Datei | Grund | Pattern |
|---|---|---|---|
| LegacyClaimFacadeBean.java | ProcessClaimDecisionHandler.java | Monster Method zerlegt | Command Handler |
| LegacyClaimStatusIfElse.java | ClaimTransitionPolicy.java | Statuslogik getrennt | Policy Object / State |
| LegacyDocumentShareClient.java | DocumentStoragePort + FileShareDocumentAdapter | FileShare entkoppelt | Port/Adapter |
| FraudRiskSoapClient.java | FraudRiskPort + FraudRiskSoapAdapter | SOAP entkoppelt | Anti-Corruption Layer |
| PolicyCoverageSoapClient.java | CoveragePort + CoverageSoapAdapter | Coverage getrennt | Port/Adapter |
| PaymentReleaseSoapClient.java | PaymentReleasePort + PaymentReleaseSoapAdapter | Zahlung entkoppelt | Idempotency / Adapter |
| LegacyNotificationSenderBean.java | NotificationPort + NotificationAdapter | Notification getrennt | Outbox / Port |
| SlaEscalationTimerBean.java | EscalationBatchJob + EscalationPolicy | TimerBean getrennt | Batch Worker / Policy |
| AuditReportStoredProcedureDao.java | AuditLogPort + StructuredAuditLogAdapter | Audit fachlich gemacht | Audit Boundary |
| LegacyClaimSearchDao.java | ClaimSearchPort + SearchIndexAdapter | Suche getrennt | Read Model / Adapter |
| CustomerMasterDataSoapClient.java | CustomerMasterDataPort + CustomerMasterDataSoapAdapter | Kundendaten entkoppelt | ACL |
| JaasRoleChecker.java | SecurityContextPort + AuthorizationPolicy | Rollenlogik getrennt | Security Boundary |
Changelog-V2-Vorbereitung aus Runde 4
Diese Runde liefert Stoff für folgende Changelog-V2-Einträge:
| ID | Änderung | Kategorie |
|---|---|---|
| CHG-004-001 | Maven Parent und Module eingeführt | Build / Struktur |
| CHG-004-002 | Domain-Modul frameworkfrei gemacht | Architektur |
| CHG-004-003 | Application-Modul mit Use Cases und Ports eingeführt | Architektur |
| CHG-004-004 | Infrastructure-Modul für Adapter eingeführt | Integration |
| CHG-004-005 | SOAP Boundary kompatibel gehalten | Migration |
| CHG-004-006 | REST/OpenAPI ergänzt | Modernisierung |
| CHG-004-007 | Claim Workflow sichtbar gemacht | Facharchitektur |
| CHG-004-008 | Document Adapter getrennt | Integration |
| CHG-004-009 | Payment mit Idempotency vorbereitet | Integration |
| CHG-004-010 | Audit Trail strukturiert | Compliance |
| CHG-004-011 | Search als Port modelliert | Performance / Entkopplung |
| CHG-004-012 | SecurityContextPort und AuthorizationPolicy eingeführt | Security |
| CHG-004-013 | Batch/SLA als Worker vorbereitet | Betrieb |
| CHG-004-014 | Architekturtests ergänzt | Qualität |
Risiken der Zielstruktur
Zu viele Module
Risiko:
Für kleine Teams kann eine zu feine Modulstruktur schwer zu pflegen sein.
Gegenmaßnahme:
- im Lernprojekt sichtbar lassen
- in echtem Projekt später schneiden
- Modulgrenzen nach Änderungsfrequenz und Teamstruktur prüfen
Falscher Shared Kernel
Risiko:
shared-kernel wird zum Müllplatz für alles.
Gegenmaßnahme:
- strenge Regeln
- Code Reviews
- ArchUnit Tests
Ports ohne fachlichen Sinn
Risiko:
Man erzeugt Ports nur, weil es „hexagonal“ klingt.
Gegenmaßnahme:
- Ports nur dort, wo Austauschbarkeit, Testbarkeit oder Runtime-Grenze relevant ist
REST zu früh als Ersatz für SOAP
Risiko:
Partner-Systeme brechen.
Gegenmaßnahme:
- SOAP-Kompatibilität erhalten
- REST parallel aufbauen
- Contract Tests schreiben
Infrastructure schleicht zurück in Application
Risiko:
Application-Code importiert wieder SOAP, JDBC oder JMS.
Gegenmaßnahme:
- Architekturtests
- Modulabhängigkeiten in Maven streng halten
Ergebnis der Runde 4
Diese Runde hat das moderne Zielprojekt mit modularer Maven-Struktur definiert.
Erzeugt wurden:
- Root-Struktur
- Parent-POM
- Modul-POMs
- Domain-Modell
- Application Use Case
- Ports
- Workflow-Modul
- Document-Modul
- Payment-Modul
- Audit-Modul
- Search-Modul
- Notification-Modul
- REST API
- SOAP-kompatible Boundary
- OpenAPI-Beispiel
- Batch-Struktur
- Infrastructure-Struktur
- Security Boundary
- Outbox-Struktur
- Tests
- Architekturtests
- Before/After-Mapping-Vorbereitung
- Changelog-V2-Vorbereitung
066. Migration nach OpenShift
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Migration nach OpenShift in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Projekt: Legacy Claims & Customer Support Enterprise System Runde: 05 Thema: Migration nach OpenShift Datum/Reihenfolge: Runde 05 nach Runde 04 — Modernes Zielprojekt mit modularer Maven-Struktur
Ziel dieser Runde
Diese Runde beschreibt die Migration des modernisierten Zielprojekts nach Red Hat OpenShift. Der Fokus liegt nicht nur auf „Dockerfile schreiben und deployen“, sondern auf einer realistischen Enterprise-Migration von einer alten WebSphere-Traditional-Landschaft in eine containerisierte Plattform.
Die Runde baut auf den vorherigen Runden auf:
- Runde 1: Zielbild, Systemlandschaft und Gesamtstruktur
- Runde 2: Legacy-Ausgangssystem mit WebSphere/EAR/EJB/WAR, SOAP, JMS, JTA, Oracle, Dokumentenlogik und Monster Methods
- Runde 3: Refactoring-Schritte und Sicherheitsnetz
- Runde 4: modernes Zielprojekt mit modularer Maven-Struktur
- Runde 5: Migration nach OpenShift
Die Migration soll sichtbar machen:
- Welche Legacy-Runtime-Abhängigkeiten entfernt oder ersetzt werden müssen.
- Welche Module containerfähig sind.
- Welche Konfigurationen nicht mehr in WebSphere liegen dürfen.
- Wie JNDI, JAAS, EJB Timer, JMS, File Shares, SOAP, Oracle und Batchjobs in OpenShift abgebildet werden.
- Wie man SOAP-kompatibel bleibt und trotzdem REST/OpenAPI ergänzt.
- Wie Deployment, Health Checks, Observability, Security und Betrieb aufgebaut werden.
- Welche Migrationstests nötig sind, damit die Umstellung nicht blind erfolgt.
Migration: Leitidee
Von WebSphere Traditional zu OpenShift
Vorher war das System stark an WebSphere Traditional gebunden:
Legacy EAR
├─ WAR: Partner Portal JSP/Servlet
├─ WAR: Internal Backoffice JSP/Servlet
├─ EJB-JAR: Claim Facade, TimerBeans, MDBs
├─ SOAP Endpoints über JAX-WS
├─ JNDI DataSources
├─ JAAS / LDAP Rollen
├─ IBM MQ JMS Resources
├─ Oracle Stored Procedures
├─ File Shares für Dokumente
└─ manuelle Deployment-/Config-Schritte in WebSphere ConsoleNach der Migration soll die Plattform trennbarer und automatisierbarer sein:
OpenShift Namespace: claims-prod
├─ claim-service
│ ├─ REST API
│ ├─ SOAP Compatibility API
│ ├─ Application Use Cases
│ ├─ Domain Logic
│ ├─ Infrastructure Adapters
│ └─ Health / Metrics / Logs
│
├─ claim-batch
│ ├─ SLA Escalation Worker
│ ├─ Outbox Publisher Worker
│ └─ CronJob / Scheduled Job
│
├─ claim-search-indexer
│ ├─ Search Read Model Updater
│ └─ Optional OpenSearch Adapter
│
├─ Oracle / external DB connection
├─ IBM MQ / AMQ / external message broker
├─ Object Storage / File Share Adapter
├─ Identity Provider integration
├─ ConfigMaps / Secrets
├─ Routes / Services
├─ NetworkPolicies
└─ Observability stackWichtig: Migration bedeutet nicht, dass alles sofort Microservice wird. Für dieses Lernprojekt ist eine kontrollierte modulare Monolith-zu-Service-Migration sinnvoller:
- zuerst Fachlogik aus WebSphere lösen,
- dann in testbare Module bringen,
- dann als containerfähige Runtime deployen,
- erst danach einzelne technische Grenzen trennen.
Migrationsstrategie
Empfohlene Strategie: Strangler + modulare Runtime
Die Migration erfolgt in Schritten:
Legacy stabilisieren Characterization Tests, Golden Master Tests, SOAP Contract Tests.
Runtime-Seams schaffen Fachlogik darf nicht mehr direkt von WebSphere APIs abhängig sein.
Konfiguration externalisieren Keine harten JNDI-Namen, keine festen File-Pfade, keine lokalen Properties im EAR.
Containerfähige Zielruntime bauen Open Liberty oder Spring Boot/Jakarta Hybrid.
SOAP-kompatible Fassade beibehalten Partner und Altsysteme werden nicht sofort gebrochen.
REST/OpenAPI ergänzen Neue Clients nutzen moderne API, alte Clients nutzen SOAP weiter.
Batch/Scheduler nach OpenShift CronJob verschieben EJB Timer wird nicht 1:1 übernommen.
JMS/Outbox sauber machen Events werden nicht mehr innerhalb der Monster Method blind publiziert.
Security modernisieren JAAS/WebSphere-Rollen werden in SecurityContextPort und AuthorizationPolicy überführt.
Betriebsfähigkeit herstellen Health, Metrics, Logs, Tracing, Rollback, Ressourcenlimits, Probes.
Ziel-Runtime-Optionen
Option A: Open Liberty als Jakarta-Runtime
Diese Option ist für WebSphere-Migrationen oft didaktisch sauber, weil viele Java-EE/Jakarta-Konzepte wiedererkennbar bleiben.
Vorteile:
- JAX-RS, CDI, JPA, Bean Validation, JAX-WS teilweise näher an Legacy-Welt
- gute Brücke von Java EE zu Jakarta
- geeignet für SOAP-Kompatibilität
- leichter zu erklären für EAR/EJB-Migration
Nachteile:
- trotzdem müssen EJB-Spezifika reduziert werden
- nicht alle alten WebSphere-Features sind 1:1 portabel
- manche Teams bevorzugen Spring Boot
Option B: Spring Boot als moderne Runtime
Vorteile:
- breites Ökosystem
- starke OpenShift- und Cloud-Native-Unterstützung
- Actuator für Health/Metrics
- gute REST/OpenAPI-Unterstützung
Nachteile:
- größerer mentaler Sprung von EJB/JAX-WS
- SOAP-Kompatibilität muss bewusst gebaut werden
- JTA/JMS/JPA-Verhalten muss gut verstanden werden
Empfehlung für dieses Lernprojekt
Für das Lernprojekt wird ein Hybrid-Zielbild dokumentiert:
- Open Liberty als Hauptvariante für WebSphere-nahe Migration.
- Spring Boot als alternative Runtime-Variante für moderne Teams.
- Domain/Application/Ports bleiben unabhängig von beiden.
Das ist wichtig, weil Architektur nicht an das Framework gekettet werden soll.
Ziel-Deployment-Struktur
OpenShift-Deployment als kontrollierter Prozess
Der Zielbetrieb trennt Build, Konfiguration, Deployment, Verifikation und Rollout, damit Änderungen nachvollziehbar und rückrollbar bleiben.
- Images bleiben unveränderlich; Konfiguration und Secrets liegen außerhalb.
- Readiness und Liveness prüfen unterschiedliche Zustände.
- Rollout und Rollback werden mit Metriken und Smoke Tests abgesichert.
migration_to_openshift/
├─ 01_container/
│ ├─ Containerfile.openliberty
│ ├─ Containerfile.springboot
│ ├─ liberty-server.xml
│ └─ jvm.options
│
├─ 02_openshift_manifests/
│ ├─ namespace.yaml
│ ├─ serviceaccount.yaml
│ ├─ configmap-claim-service.yaml
│ ├─ secret-claim-service.yaml
│ ├─ deployment-claim-service.yaml
│ ├─ service-claim-service.yaml
│ ├─ route-claim-service.yaml
│ ├─ cronjob-escalation.yaml
│ ├─ cronjob-outbox-publisher.yaml
│ ├─ networkpolicy.yaml
│ ├─ hpa.yaml
│ └─ pdb.yaml
│
├─ 03_runtime_config/
│ ├─ application-openshift.yml
│ ├─ claims-runtime-boundaries.md
│ ├─ datasource-migration.md
│ ├─ mq-migration.md
│ ├─ file-storage-migration.md
│ └─ security-migration.md
│
├─ 04_observability/
│ ├─ logging.md
│ ├─ metrics.md
│ ├─ tracing.md
│ ├─ health-checks.md
│ └─ alerting.md
│
├─ 05_migration_tests/
│ ├─ SoapCompatibilityIT.java
│ ├─ RestApiSmokeIT.java
│ ├─ OutboxPublisherIT.java
│ ├─ OracleConnectivityIT.java
│ ├─ SecurityRoleMappingIT.java
│ └─ OpenShiftDeploymentSmokeTest.md
│
└─ 06_decisions/
├─ MIGRATION_DECISIONS.md
├─ RISKS.md
├─ ROLLBACK_PLAN.md
└─ CUTOVER_PLAN.mdContainerisierung
Containerfile für Open Liberty
#### migration_to_openshift/01_container/Containerfile.openliberty
FROM icr.io/appcafe/open-liberty:kernel-slim-java17-openj9-ubi
USER root
RUN mkdir -p /config/apps /opt/claims/config /opt/claims/logs \
&& chown -R 1001:0 /config /opt/claims \
&& chmod -R g=u /config /opt/claims
USER 1001
COPY --chown=1001:0 liberty-server.xml /config/server.xml
COPY --chown=1001:0 jvm.options /config/jvm.options
COPY --chown=1001:0 target/claim-service.war /config/apps/claim-service.war
ENV WLP_LOGGING_CONSOLE_FORMAT=json
ENV WLP_LOGGING_CONSOLE_LOGLEVEL=info
ENV CLAIMS_CONFIG_DIR=/opt/claims/config
EXPOSE 9080 9443
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -fsS http://localhost:9080/health/live || exit 1Erklärung
Dieses Containerfile zeigt die Open-Liberty-Variante. Wichtig ist:
- keine WebSphere-Konsole mehr,
- keine manuelle Installation von DataSources,
- Runtime-Konfiguration liegt im Image oder wird per ConfigMap eingebunden,
- Anwendung wird als WAR deployt,
- Logs gehen auf stdout/stderr,
- Container läuft nicht als root.
Liberty server.xml
<!-- migration_to_openshift/01_container/liberty-server.xml -->
<server description="Claims Customer Support Service">
<featureManager>
<feature>jakartaee-10.0</feature>
<feature>mpHealth-4.0</feature>
<feature>mpMetrics-5.0</feature>
<feature>mpConfig-3.0</feature>
<feature>mpOpenAPI-3.1</feature>
</featureManager>
<httpEndpoint id="defaultHttpEndpoint"
host="*"
httpPort="9080"
httpsPort="9443" />
<webApplication id="claim-service"
location="claim-service.war"
contextRoot="/claims" />
<variable name="oracleJdbcUrl" value="${env.ORACLE_JDBC_URL}" />
<variable name="oracleUser" value="${env.ORACLE_USER}" />
<variable name="oraclePassword" value="${env.ORACLE_PASSWORD}" />
<dataSource id="ClaimsDataSource" jndiName="jdbc/ClaimsDS">
<jdbcDriver libraryRef="OracleLib" />
<properties.oracle URL="${oracleJdbcUrl}"
user="${oracleUser}"
password="${oraclePassword}" />
</dataSource>
<library id="OracleLib">
<fileset dir="/opt/claims/jdbc" includes="ojdbc*.jar" />
</library>
</server>Migrationshinweis
Im Legacy-System war jdbc/ClaimsDS in der
WebSphere-Konsole definiert. In der Zielwelt muss die DataSource
reproduzierbar sein. Entweder:
- im Liberty
server.xml, - über Operator/Secret/ConfigMap,
- oder bei Spring Boot über
application-openshift.yml.
Wichtig: Die Fachlogik darf den JNDI-Namen nicht kennen.
Containerfile für Spring Boot Alternative
#### migration_to_openshift/01_container/Containerfile.springboot
FROM eclipse-temurin:17-jre-ubi9-minimal
WORKDIR /app
RUN mkdir -p /app/config /app/logs \
&& chown -R 1001:0 /app \
&& chmod -R g=u /app
USER 1001
COPY --chown=1001:0 target/claim-service.jar /app/claim-service.jar
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
ENV SPRING_PROFILES_ACTIVE=openshift
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/claim-service.jar"]Erklärung
Die Spring-Boot-Variante wird als executable JAR betrieben. Diese Variante ist gut für neue REST-/OpenAPI-Services. Für SOAP-Kompatibilität braucht sie aber bewusst gepflegte SOAP-Boundaries oder einen separaten SOAP-Adapter.
OpenShift Namespace und Basisobjekte
Namespace
#### migration_to_openshift/02_openshift_manifests/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: claims-prod
labels:
app.kubernetes.io/part-of: legacy-claims-customer-support
environment: prodServiceAccount
#### migration_to_openshift/02_openshift_manifests/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: claim-service-sa
namespace: claims-prod
labels:
app.kubernetes.io/name: claim-serviceConfigMap und Secret
ConfigMap
#### migration_to_openshift/02_openshift_manifests/configmap-claim-service.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: claim-service-config
namespace: claims-prod
data:
CLAIMS_ENVIRONMENT: "prod"
CLAIMS_LOG_LEVEL: "INFO"
CLAIMS_DEFAULT_LOCALE: "de_AT"
CLAIMS_DOCUMENT_STORAGE_MODE: "file-share-adapter"
CLAIMS_PAYMENT_IDEMPOTENCY_ENABLED: "true"
CLAIMS_OUTBOX_ENABLED: "true"
CLAIMS_SOAP_COMPATIBILITY_ENABLED: "true"
CLAIMS_REST_API_ENABLED: "true"
CLAIMS_SLA_ESCALATION_ENABLED: "true"
CLAIMS_SEARCH_ADAPTER: "oracle-read-model"
FRAUD_RISK_TIMEOUT_MS: "2500"
COVERAGE_TIMEOUT_MS: "3000"
PAYMENT_RELEASE_TIMEOUT_MS: "5000"
NOTIFICATION_TIMEOUT_MS: "2000"Secret
#### migration_to_openshift/02_openshift_manifests/secret-claim-service.yaml
apiVersion: v1
kind: Secret
metadata:
name: claim-service-secret
namespace: claims-prod
type: Opaque
stringData:
ORACLE_JDBC_URL: "jdbc:oracle:thin:@//oracle.example.internal:1521/CLAIMS"
ORACLE_USER: "claims_app"
ORACLE_PASSWORD: "CHANGE_ME"
IBM_MQ_HOST: "mq.example.internal"
IBM_MQ_PORT: "1414"
IBM_MQ_CHANNEL: "CLAIMS.SVRCONN"
IBM_MQ_QUEUE_MANAGER: "CLAIMSQM"
IBM_MQ_USER: "claims_mq"
IBM_MQ_PASSWORD: "CHANGE_ME"
LDAP_URL: "ldaps://ldap.example.internal:636"
LDAP_BIND_DN: "cn=claims-service,ou=svc,dc=example,dc=internal"
LDAP_BIND_PASSWORD: "CHANGE_ME"Sicherheitsregel
In echten Projekten dürfen Secrets nicht im Git-Repository liegen. Für das Lernprojekt werden sie als Beispiel gezeigt, aber in der Praxis gehören sie in:
- OpenShift Secret Management,
- External Secrets Operator,
- Vault,
- Sealed Secrets,
- oder ein anderes freigegebenes Secret-Verfahren.
Claim Service Deployment
Deployment
#### migration_to_openshift/02_openshift_manifests/deployment-claim-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: claim-service
namespace: claims-prod
labels:
app.kubernetes.io/name: claim-service
app.kubernetes.io/part-of: legacy-claims-customer-support
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app.kubernetes.io/name: claim-service
template:
metadata:
labels:
app.kubernetes.io/name: claim-service
app.kubernetes.io/part-of: legacy-claims-customer-support
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/metrics"
prometheus.io/port: "9080"
spec:
serviceAccountName: claim-service-sa
containers:
- name: claim-service
image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-service:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 9080
- name: https
containerPort: 9443
envFrom:
- configMapRef:
name: claim-service-config
- secretRef:
name: claim-service-secret
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1536Mi"
readinessProbe:
httpGet:
path: /claims/health/ready
port: http
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet:
path: /claims/health/live
port: http
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /claims/health/started
port: http
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 18
volumeMounts:
- name: tmp
mountPath: /tmp
- name: claims-runtime
mountPath: /opt/claims/runtime
volumes:
- name: tmp
emptyDir: {}
- name: claims-runtime
emptyDir: {}Erklärung
Wichtige Migrationsentscheidungen:
replicas: 2erzwingt, dass die Anwendung nicht mehr implizit Single-Node-Logik haben darf.maxUnavailable: 0reduziert Downtime beim Rolling Update.readinessProbeverhindert Traffic auf nicht bereite Pods.livenessProbeerkennt hängende Prozesse.startupProbeschützt langsame Starts vor falschen Restarts.emptyDirersetzt keine Dokumentenablage, sondern nur temporäre Runtime-Dateien.- Dokumente dürfen nicht mehr hart in lokalen Pfaden gespeichert werden.
Service und Route
Service
#### migration_to_openshift/02_openshift_manifests/service-claim-service.yaml
apiVersion: v1
kind: Service
metadata:
name: claim-service
namespace: claims-prod
labels:
app.kubernetes.io/name: claim-service
spec:
selector:
app.kubernetes.io/name: claim-service
ports:
- name: http
port: 80
targetPort: 9080
- name: https
port: 443
targetPort: 9443Route
#### migration_to_openshift/02_openshift_manifests/route-claim-service.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: claim-service
namespace: claims-prod
labels:
app.kubernetes.io/name: claim-service
spec:
host: claims.example.at
to:
kind: Service
name: claim-service
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: RedirectSOAP- und REST-Pfade
Die Route kann beide Welten bedienen:
SOAP altkompatibel:
https://claims.example.at/claims/soap/ClaimService
REST neu:
https://claims.example.at/claims/api/v1/claims/{claimId}/decision
OpenAPI:
https://claims.example.at/claims/openapi
Health:
https://claims.example.at/claims/health/readyHealth Checks im Code
Health Resource
package com.seb4u.demo.claims.infrastructure.health;
import com.seb4u.demo.claims.application.port.out.ClaimRepository;
import com.seb4u.demo.claims.application.port.out.OutboxPort;
import com.seb4u.demo.claims.application.port.out.PaymentReleasePort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;
@ApplicationScoped
@Liveness
public class ClaimServiceLivenessCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("claim-service-live")
.up()
.withData("runtime", "openliberty")
.withData("service", "claim-service")
.build();
}
}
@ApplicationScoped
@Readiness
class ClaimServiceReadinessCheck implements HealthCheck {
private final ClaimRepository claimRepository;
private final OutboxPort outboxPort;
private final PaymentReleasePort paymentReleasePort;
@Inject
ClaimServiceReadinessCheck(
ClaimRepository claimRepository,
OutboxPort outboxPort,
PaymentReleasePort paymentReleasePort
) {
this.claimRepository = claimRepository;
this.outboxPort = outboxPort;
this.paymentReleasePort = paymentReleasePort;
}
@Override
public HealthCheckResponse call() {
HealthCheckResponse.Builder response = HealthCheckResponse.named("claim-service-ready");
boolean repositoryReady = claimRepository.canConnect();
boolean outboxReady = outboxPort.canPersistEvents();
boolean paymentPortConfigured = paymentReleasePort.isConfigured();
boolean ready = repositoryReady && outboxReady && paymentPortConfigured;
response.status(ready)
.withData("oracle", repositoryReady)
.withData("outbox", outboxReady)
.withData("paymentPortConfigured", paymentPortConfigured);
return response.build();
}
}Warum so?
Liveness darf nicht zu viele externe Systeme prüfen. Sonst würde ein externer Ausfall den Pod unnötig neu starten. Readiness darf externe Abhängigkeiten prüfen, weil der Pod keinen Traffic bekommen soll, wenn er fachlich nicht arbeitsfähig ist.
Externe Konfiguration statt WebSphere JNDI
Legacy vorher
package com.seb4u.demo.claims.legacy.infrastructure;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class LegacyDataSourceLookup {
public DataSource lookupClaimsDataSource() {
try {
InitialContext ctx = new InitialContext();
return (DataSource) ctx.lookup("jdbc/claimsLegacyDS");
} catch (Exception e) {
throw new IllegalStateException("Could not lookup WebSphere DataSource", e);
}
}
}Problem:
- JNDI-Name ist technische Laufzeitkopplung.
- Tests brauchen WebSphere oder komplizierte Mocks.
- Migration nach OpenShift wird erschwert.
Nachher: Configuration Object
package com.seb4u.demo.claims.infrastructure.config;
import java.time.Duration;
import java.util.Objects;
public final class ClaimsRuntimeConfiguration {
private final String oracleJdbcUrl;
private final String oracleUser;
private final String oraclePassword;
private final Duration fraudRiskTimeout;
private final Duration coverageTimeout;
private final Duration paymentTimeout;
private final boolean outboxEnabled;
private final boolean soapCompatibilityEnabled;
public ClaimsRuntimeConfiguration(
String oracleJdbcUrl,
String oracleUser,
String oraclePassword,
Duration fraudRiskTimeout,
Duration coverageTimeout,
Duration paymentTimeout,
boolean outboxEnabled,
boolean soapCompatibilityEnabled
) {
this.oracleJdbcUrl = requireNonBlank(oracleJdbcUrl, "oracleJdbcUrl");
this.oracleUser = requireNonBlank(oracleUser, "oracleUser");
this.oraclePassword = requireNonBlank(oraclePassword, "oraclePassword");
this.fraudRiskTimeout = Objects.requireNonNull(fraudRiskTimeout);
this.coverageTimeout = Objects.requireNonNull(coverageTimeout);
this.paymentTimeout = Objects.requireNonNull(paymentTimeout);
this.outboxEnabled = outboxEnabled;
this.soapCompatibilityEnabled = soapCompatibilityEnabled;
}
public String oracleJdbcUrl() {
return oracleJdbcUrl;
}
public String oracleUser() {
return oracleUser;
}
public String oraclePassword() {
return oraclePassword;
}
public Duration fraudRiskTimeout() {
return fraudRiskTimeout;
}
public Duration coverageTimeout() {
return coverageTimeout;
}
public Duration paymentTimeout() {
return paymentTimeout;
}
public boolean outboxEnabled() {
return outboxEnabled;
}
public boolean soapCompatibilityEnabled() {
return soapCompatibilityEnabled;
}
private static String requireNonBlank(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
return value;
}
}Vorteil
- Konfiguration ist testbar.
- Fehlende Konfiguration wird beim Start erkannt.
- Ports/Adapters bekommen klare Werte.
- OpenShift ConfigMaps/Secrets lassen sich sauber abbilden.
Datenbankmigration
Ausgangslage
Legacy nutzt:
- Oracle Tabellen,
- Stored Procedures,
- Reporting Tables,
- gemischtes JDBC/JPA,
- teils implizite Transaktionslogik in Stored Procedures,
- technische Statusfelder,
- Audit-Informationen in Nebentabellen.
Zielbild
Die Migration trennt:
Fachliches Modell
├─ Claim Aggregate
├─ Claim Status
├─ Claim Decision
├─ Claim Audit Trail
└─ Claim Outbox Event
Persistenzmodell
├─ CLAIM
├─ CLAIM_DECISION
├─ CLAIM_DOCUMENT
├─ CLAIM_AUDIT_EVENT
├─ CLAIM_OUTBOX
└─ CLAIM_SEARCH_READ_MODEL
Legacy-Kompatibilität
├─ Legacy Views
├─ Übergangs-Stored-Procedures
└─ Vergleichstests gegen Golden MasterMigration mit Flyway/Liquibase als Konzept
-- migration_to_openshift/03_runtime_config/db/V001__create_claim_outbox.sql
CREATE TABLE CLAIM_OUTBOX (
OUTBOX_ID VARCHAR2(64) PRIMARY KEY,
AGGREGATE_ID VARCHAR2(64) NOT NULL,
AGGREGATE_TYPE VARCHAR2(64) NOT NULL,
EVENT_TYPE VARCHAR2(128) NOT NULL,
EVENT_PAYLOAD CLOB NOT NULL,
IDEMPOTENCY_KEY VARCHAR2(128) NOT NULL,
STATUS VARCHAR2(32) NOT NULL,
CREATED_AT TIMESTAMP NOT NULL,
PUBLISHED_AT TIMESTAMP NULL,
RETRY_COUNT NUMBER(10) DEFAULT 0 NOT NULL,
LAST_ERROR VARCHAR2(2000) NULL
);
CREATE UNIQUE INDEX UX_CLAIM_OUTBOX_IDEMPOTENCY
ON CLAIM_OUTBOX (IDEMPOTENCY_KEY);-- migration_to_openshift/03_runtime_config/db/V002__create_claim_audit_event.sql
CREATE TABLE CLAIM_AUDIT_EVENT (
AUDIT_EVENT_ID VARCHAR2(64) PRIMARY KEY,
CLAIM_ID VARCHAR2(64) NOT NULL,
ACTOR_ID VARCHAR2(128) NOT NULL,
ACTOR_ROLE VARCHAR2(128) NOT NULL,
ACTION VARCHAR2(128) NOT NULL,
BUSINESS_REASON VARCHAR2(1000),
CREATED_AT TIMESTAMP NOT NULL,
CORRELATION_ID VARCHAR2(128) NOT NULL
);
CREATE INDEX IX_CLAIM_AUDIT_EVENT_CLAIM
ON CLAIM_AUDIT_EVENT (CLAIM_ID, CREATED_AT);Datenbank-Migrationsregel
Stored Procedures werden nicht blind gelöscht. Für jede Procedure gibt es eine Entscheidung:
| Legacy Stored Procedure | Ziel | Strategie |
|---|---|---|
SP_PROCESS_CLAIM_DECISION |
Application Handler + Domain Policies | Schrittweise ersetzen, Golden Master vergleichen |
SP_REFRESH_CLAIM_REPORTING |
Reporting Read Model | Übergangsweise behalten, später Read Model Worker |
SP_ESCALATE_OVERDUE_CLAIMS |
EscalationBatchJob + EscalationPolicy | Logik aus DB herausziehen |
SP_EXPORT_AUDIT_CSV |
AuditReportingAdapter | Reporting Boundary schaffen |
JMS / IBM MQ Migration
Legacy vorher
package com.seb4u.demo.claims.legacy.messaging;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.JMSContext;
import javax.jms.Queue;
@Stateless
public class LegacyClaimEventPublisher {
@Resource(lookup = "jms/ClaimsEventQueue")
private Queue claimsEventQueue;
@Resource
private JMSContext jmsContext;
public void publishDecisionApproved(String claimId, String paymentId) {
jmsContext.createProducer()
.setProperty("claimId", claimId)
.setProperty("paymentId", paymentId)
.send(claimsEventQueue, "CLAIM_APPROVED:" + claimId + ":" + paymentId);
}
}Problem:
- Event wird direkt im Fachfluss publiziert.
- Bei Transaktionsfehlern kann DB-Status und Event auseinanderlaufen.
- Format ist technisch und instabil.
- Kein Idempotency-Konzept.
Nachher: Outbox Port
package com.seb4u.demo.claims.application.port.out;
import com.seb4u.demo.claims.shared.kernel.CorrelationId;
import java.time.Instant;
public interface OutboxPort {
void append(OutboxMessage message);
boolean canPersistEvents();
record OutboxMessage(
String outboxId,
String aggregateId,
String aggregateType,
String eventType,
String payloadJson,
String idempotencyKey,
CorrelationId correlationId,
Instant createdAt
) {
}
}package com.seb4u.demo.claims.infrastructure.outbox;
import com.seb4u.demo.claims.application.port.out.OutboxPort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
@ApplicationScoped
public class JdbcOutboxAdapter implements OutboxPort {
private final DataSource dataSource;
@Inject
public JdbcOutboxAdapter(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void append(OutboxMessage message) {
String sql = """
INSERT INTO CLAIM_OUTBOX
(OUTBOX_ID, AGGREGATE_ID, AGGREGATE_TYPE, EVENT_TYPE, EVENT_PAYLOAD,
IDEMPOTENCY_KEY, STATUS, CREATED_AT)
VALUES (?, ?, ?, ?, ?, ?, 'NEW', ?)
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, message.outboxId());
ps.setString(2, message.aggregateId());
ps.setString(3, message.aggregateType());
ps.setString(4, message.eventType());
ps.setString(5, message.payloadJson());
ps.setString(6, message.idempotencyKey());
ps.setObject(7, message.createdAt());
ps.executeUpdate();
} catch (Exception e) {
throw new OutboxPersistenceException("Could not append outbox message", e);
}
}
@Override
public boolean canPersistEvents() {
try (Connection connection = dataSource.getConnection()) {
return connection.isValid(2);
} catch (Exception e) {
return false;
}
}
}Outbox Publisher als OpenShift CronJob
#### migration_to_openshift/02_openshift_manifests/cronjob-outbox-publisher.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: claim-outbox-publisher
namespace: claims-prod
spec:
schedule: "*/2 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/name: claim-outbox-publisher
spec:
restartPolicy: Never
serviceAccountName: claim-service-sa
containers:
- name: outbox-publisher
image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-batch:1.0.0
args:
- "run"
- "outbox-publisher"
envFrom:
- configMapRef:
name: claim-service-config
- secretRef:
name: claim-service-secret
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "768Mi"SLA & Escalation Scheduler Migration
Legacy vorher
package com.seb4u.demo.claims.legacy.scheduler;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
@Singleton
public class SlaEscalationTimerBean {
@Schedule(hour = "*/1", persistent = false)
public void checkOverdueClaims() {
// Oracle Query
// Stored Procedure
// direkte Statusänderung
// direkte JMS-Benachrichtigung
// direkte Teamleiter-Eskalation
}
}Problem:
- Timer hängt an EJB/WebSphere.
- Keine klare Skalierungs- und Locking-Strategie.
- Bei mehreren Instanzen drohen doppelte Eskalationen.
- Schwer testbar.
Nachher: EscalationBatchJob
package com.seb4u.demo.claims.batch.escalation;
import com.seb4u.demo.claims.application.port.out.ClaimRepository;
import com.seb4u.demo.claims.application.port.out.NotificationPort;
import com.seb4u.demo.claims.application.port.out.OutboxPort;
import com.seb4u.demo.claims.workflow.escalation.EscalationPolicy;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
public class EscalationBatchJob {
private final ClaimRepository claimRepository;
private final EscalationPolicy escalationPolicy;
private final OutboxPort outboxPort;
private final NotificationPort notificationPort;
private final Clock clock;
public EscalationBatchJob(
ClaimRepository claimRepository,
EscalationPolicy escalationPolicy,
OutboxPort outboxPort,
NotificationPort notificationPort,
Clock clock
) {
this.claimRepository = claimRepository;
this.escalationPolicy = escalationPolicy;
this.outboxPort = outboxPort;
this.notificationPort = notificationPort;
this.clock = clock;
}
public BatchResult run(BatchExecutionContext context) {
Instant now = clock.instant();
List<OpenClaimProjection> openClaims = claimRepository.findOpenClaimsForEscalation(context.partitionSize());
int escalated = 0;
int skipped = 0;
for (OpenClaimProjection projection : openClaims) {
EscalationDecision decision = escalationPolicy.evaluate(projection, now);
if (decision.shouldEscalate()) {
claimRepository.markEscalated(projection.claimId(), decision.reason(), context.executionId());
outboxPort.append(decision.toOutboxMessage(context.correlationId()));
notificationPort.prepareEscalationNotification(projection.claimId(), decision.reason());
escalated++;
} else {
skipped++;
}
}
return new BatchResult(context.executionId(), openClaims.size(), escalated, skipped);
}
}OpenShift CronJob
#### migration_to_openshift/02_openshift_manifests/cronjob-escalation.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: claim-sla-escalation
namespace: claims-prod
spec:
schedule: "0 * * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1
template:
metadata:
labels:
app.kubernetes.io/name: claim-sla-escalation
spec:
restartPolicy: Never
serviceAccountName: claim-service-sa
containers:
- name: escalation-job
image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-batch:1.0.0
args:
- "run"
- "sla-escalation"
- "--partition-size=500"
envFrom:
- configMapRef:
name: claim-service-config
- secretRef:
name: claim-service-secret
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "750m"
memory: "1Gi"Wichtig
Für echte Produktion ist ein zusätzliches Lease-Lock sinnvoll, falls mehrere Jobs oder manuelle Starts möglich sind.
Lease Lock gegen doppelte Batchausführung
CREATE TABLE CLAIM_BATCH_LOCK (
LOCK_NAME VARCHAR2(128) PRIMARY KEY,
LOCK_OWNER VARCHAR2(128) NOT NULL,
LOCK_UNTIL TIMESTAMP NOT NULL,
UPDATED_AT TIMESTAMP NOT NULL
);package com.seb4u.demo.claims.batch.lock;
import java.time.Duration;
import java.time.Instant;
public interface BatchLeaseLockPort {
boolean tryAcquire(String lockName, String owner, Duration leaseDuration, Instant now);
void release(String lockName, String owner);
}package com.seb4u.demo.claims.batch.lock;
public final class LockedBatchRunner {
private final BatchLeaseLockPort lockPort;
public LockedBatchRunner(BatchLeaseLockPort lockPort) {
this.lockPort = lockPort;
}
public BatchResult runWithLock(String lockName, String owner, BatchExecutable executable) {
boolean acquired = lockPort.tryAcquire(lockName, owner, java.time.Duration.ofMinutes(30), java.time.Instant.now());
if (!acquired) {
return BatchResult.skipped("Lock not acquired: " + lockName);
}
try {
return executable.run();
} finally {
lockPort.release(lockName, owner);
}
}
}Document Storage Migration
Legacy Problem
Das Legacy-System verwendet:
- harte File-Share-Pfade,
- Dokumentpfade in Datenbankspalten,
- XML-Metadaten neben Dateien,
- Batch-Archivierung,
- unklare Versionierung.
Beispiel:
String path = "//legacy-share/claims/" + claimId + "/" + uploadedFileName;
Files.copy(inputStream, Path.of(path));Problem:
- OpenShift Pods sind kurzlebig.
- Lokales Dateisystem ist nicht dauerhaft.
- Hardcoded Pfade brechen in Containern.
- Zugriff auf alte Shares braucht Security- und Netzwerkfreigaben.
Zielbild
DocumentStoragePort
├─ FileShareDocumentAdapter Übergangsadapter
├─ ArchiveDocumentAdapter Legacy Archive Service
├─ ObjectStorageDocumentAdapter später S3/NooBaa/MinIO
└─ DocumentVerificationPort fachliche Prüfung separatOpenShift-Konfiguration für Übergangs-FileShare
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claims-document-share-pvc
namespace: claims-prod
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
storageClassName: managed-nfs-storage### Ausschnitt im Deployment
volumeMounts:
- name: document-share
mountPath: /mnt/claims-documents
volumes:
- name: document-share
persistentVolumeClaim:
claimName: claims-document-share-pvcMigrationsentscheidung
Der FileShare-Adapter ist nur eine Übergangsstrategie. Die langfristige Richtung ist Object Storage.
Security Migration
Legacy vorher
package com.seb4u.demo.claims.legacy.security;
import javax.ejb.SessionContext;
public class JaasRoleChecker {
private final SessionContext sessionContext;
public JaasRoleChecker(SessionContext sessionContext) {
this.sessionContext = sessionContext;
}
public boolean canApprovePayment() {
return sessionContext.isCallerInRole("CLAIMS_MANAGER")
|| sessionContext.isCallerInRole("PAYMENT_APPROVER");
}
}Problem:
- WebSphere SessionContext ist direkt in Fachentscheidung sichtbar.
- Rollen sind hart kodiert.
- Partnerrollen, Backoffice-Rollen und Systemrollen vermischen sich.
Nachher: SecurityContextPort + AuthorizationPolicy
package com.seb4u.demo.claims.application.port.out;
import java.util.Set;
public interface SecurityContextPort {
AuthenticatedActor currentActor();
record AuthenticatedActor(
String actorId,
String displayName,
Set<String> technicalRoles,
Set<String> businessPermissions,
String authenticationSource
) {
public boolean hasPermission(String permission) {
return businessPermissions.contains(permission);
}
}
}package com.seb4u.demo.claims.application.security;
import com.seb4u.demo.claims.application.port.out.SecurityContextPort;
import com.seb4u.demo.claims.domain.claim.Claim;
public class AuthorizationPolicy {
public void requireDecisionPermission(
SecurityContextPort.AuthenticatedActor actor,
Claim claim,
String requestedDecision
) {
if ("APPROVE_PAYMENT".equals(requestedDecision)
&& !actor.hasPermission("claim.payment.approve")) {
throw new AccessDeniedBusinessException(
"Actor " + actor.actorId() + " is not allowed to approve claim payment");
}
if (claim.isPartnerSubmitted()
&& !actor.hasPermission("claim.partner.process")) {
throw new AccessDeniedBusinessException(
"Actor " + actor.actorId() + " is not allowed to process partner submitted claims");
}
}
}Role Mapping
#### migration_to_openshift/03_runtime_config/security-role-mapping.yaml
roleMappings:
CLAIMS_AGENT:
- claim.read
- claim.document.review
- claim.decision.prepare
CLAIMS_MANAGER:
- claim.read
- claim.document.review
- claim.decision.prepare
- claim.payment.approve
- claim.escalation.resolve
PARTNER_USER:
- claim.partner.create
- claim.partner.uploadDocument
- claim.partner.readOwnClaim
COMPLIANCE_AUDITOR:
- claim.audit.read
- claim.report.exportNetworkPolicy
#### migration_to_openshift/02_openshift_manifests/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: claim-service-network-policy
namespace: claims-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: claim-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
network.openshift.io/policy-group: ingress
ports:
- protocol: TCP
port: 9080
egress:
- to:
- ipBlock:
cidr: 10.20.0.0/16
ports:
- protocol: TCP
port: 1521
- to:
- ipBlock:
cidr: 10.30.0.0/16
ports:
- protocol: TCP
port: 1414
- to:
- ipBlock:
cidr: 10.40.0.0/16
ports:
- protocol: TCP
port: 443Erklärung
OpenShift-Migration ist nicht nur Anwendungsmigration. Netzwerkflüsse müssen explizit werden:
- Oracle DB,
- MQ,
- externe SOAP Services,
- LDAP/Identity Provider,
- Archive Service,
- Notification Gateways.
Ressourcen, HPA und PDB
Horizontal Pod Autoscaler
#### migration_to_openshift/02_openshift_manifests/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: claim-service-hpa
namespace: claims-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: claim-service
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70PodDisruptionBudget
#### migration_to_openshift/02_openshift_manifests/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: claim-service-pdb
namespace: claims-prod
spec:
minAvailable: 1
selector:
matchLabels:
app.kubernetes.io/name: claim-serviceObservability
Security, Audit und Observability zusammendenken
Identität, Autorisierung und technische Telemetrie bilden zusammen den belastbaren Nachweis einer Claims-Entscheidung.
- Die Identität liefert Rolle, Mandant und Bearbeiterkontext.
- Policies entscheiden fachlich und technisch über Berechtigungen.
- Audit Events, Logs, Metriken und Traces beantworten unterschiedliche Nachweisfragen.
Logging
Ziel:
- JSON Logs,
- Correlation ID,
- Claim ID nur kontrolliert,
- keine sensiblen Kundendaten im Log,
- klare technische und fachliche Events.
package com.seb4u.demo.claims.infrastructure.logging;
import com.seb4u.demo.claims.shared.kernel.CorrelationId;
public final class StructuredLogEvent {
private final String eventType;
private final String claimId;
private final CorrelationId correlationId;
private final String message;
public StructuredLogEvent(String eventType, String claimId, CorrelationId correlationId, String message) {
this.eventType = eventType;
this.claimId = claimId;
this.correlationId = correlationId;
this.message = message;
}
public String toJsonLine() {
return "{"
+ "\"eventType\":\"" + escape(eventType) + "\","
+ "\"claimId\":\"" + escape(claimId) + "\","
+ "\"correlationId\":\"" + escape(correlationId.value()) + "\","
+ "\"message\":\"" + escape(message) + "\""
+ "}";
}
private String escape(String value) {
return value == null ? "" : value.replace("\"", "\\\"");
}
}Metrics
Wichtige Metriken:
claims_decision_total{decision="APPROVED"}
claims_decision_total{decision="REJECTED"}
claims_payment_release_total{result="SUCCESS"}
claims_payment_release_total{result="FAILED"}
claims_fraud_risk_timeout_total
claims_document_verification_failed_total
claims_outbox_pending_total
claims_outbox_publish_failed_total
claims_sla_escalated_total
claims_soap_request_total
claims_rest_request_totalTracing
Wichtige Trace-Spans:
ProcessClaimDecision
├─ LoadClaim
├─ AuthorizeActor
├─ VerifyDocument
├─ CheckCoverage
├─ CheckFraudRisk
├─ TransitionClaimState
├─ PreparePaymentRelease
├─ AppendAuditEvent
├─ AppendOutboxEvent
└─ PersistClaimREST Smoke Test
package com.seb4u.demo.claims.migrationtests.openshift;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
class RestApiSmokeIT {
@Test
void shouldExposeHealthAndOpenApi() throws Exception {
String baseUrl = System.getProperty("claims.baseUrl", "http://localhost:9080/claims");
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> health = client.send(
HttpRequest.newBuilder(URI.create(baseUrl + "/health/ready")).GET().build(),
HttpResponse.BodyHandlers.ofString()
);
assertThat(health.statusCode()).isBetween(200, 299);
HttpResponse<String> openapi = client.send(
HttpRequest.newBuilder(URI.create(baseUrl + "/openapi")).GET().build(),
HttpResponse.BodyHandlers.ofString()
);
assertThat(openapi.statusCode()).isBetween(200, 299);
assertThat(openapi.body()).contains("Process claim decision");
}
}SOAP Compatibility Test
package com.seb4u.demo.claims.migrationtests.soap;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
class SoapCompatibilityIT {
@Test
void shouldKeepProcessClaimDecisionSoapOperationCompatible() throws Exception {
String baseUrl = System.getProperty("claims.baseUrl", "http://localhost:9080/claims");
String soapEnvelope = """
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:cla="http://seb4u.com/demo/claims">
<soapenv:Header/>
<soapenv:Body>
<cla:processClaimDecision>
<claimId>CLM-10001</claimId>
<decision>APPROVE_PAYMENT</decision>
<actorId>agent-42</actorId>
<reason>Coverage valid and documents verified</reason>
</cla:processClaimDecision>
</soapenv:Body>
</soapenv:Envelope>
""";
HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/soap/ClaimService"))
.header("Content-Type", "text/xml; charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString(soapEnvelope))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request,
HttpResponse.BodyHandlers.ofString()
);
assertThat(response.statusCode()).isBetween(200, 299);
assertThat(response.body()).contains("processClaimDecisionResponse");
}
}Oracle Connectivity Test
package com.seb4u.demo.claims.migrationtests.infrastructure;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import static org.assertj.core.api.Assertions.assertThat;
class OracleConnectivityIT {
@Test
void shouldConnectToOracleAndReadSchemaVersion() throws Exception {
String url = System.getenv("ORACLE_JDBC_URL");
String user = System.getenv("ORACLE_USER");
String password = System.getenv("ORACLE_PASSWORD");
try (Connection connection = DriverManager.getConnection(url, user, password);
ResultSet rs = connection.createStatement().executeQuery("SELECT 1 FROM DUAL")) {
assertThat(rs.next()).isTrue();
assertThat(rs.getInt(1)).isEqualTo(1);
}
}
}Security Role Mapping Test
package com.seb4u.demo.claims.migrationtests.security;
import com.seb4u.demo.claims.application.port.out.SecurityContextPort.AuthenticatedActor;
import com.seb4u.demo.claims.application.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.claim.Claim;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class SecurityRoleMappingIT {
@Test
void partnerUserMustNotApprovePayment() {
AuthenticatedActor partner = new AuthenticatedActor(
"partner-17",
"Partner User",
Set.of("PARTNER_USER"),
Set.of("claim.partner.create", "claim.partner.readOwnClaim"),
"openshift-idp"
);
Claim claim = Claim.partnerSubmitted("CLM-4711");
AuthorizationPolicy policy = new AuthorizationPolicy();
assertThatThrownBy(() -> policy.requireDecisionPermission(partner, claim, "APPROVE_PAYMENT"))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("not allowed");
}
}OpenShift Deployment Smoke Test als Checkliste
### OpenShiftDeploymentSmokeTest
### Vorbedingungen
- Namespace existiert.
- Image ist gebaut und gepusht.
- ConfigMap existiert.
- Secret existiert.
- Route ist erreichbar.
- Oracle-Verbindung ist freigeschaltet.
- MQ-Verbindung ist freigeschaltet.
- LDAP/Identity Provider ist erreichbar.
### Prüfungen
1. oc get pods -n claims-prod
2. Alle Pods Running und Ready.
3. oc logs deployment/claim-service -n claims-prod zeigt keine Startfehler.
4. /claims/health/live liefert OK.
5. /claims/health/ready liefert OK.
6. /claims/openapi erreichbar.
7. SOAP WSDL erreichbar.
8. Testentscheidung für Test-Claim läuft durch.
9. Outbox-Event wird geschrieben.
10. Outbox-CronJob publiziert Event.
11. Audit-Event wird gespeichert.
12. Search Read Model wird aktualisiert.
13. Keine sensiblen Kundendaten im Log.
14. Rollback auf vorherige Deployment-Revision möglich.Cutover Plan
Phasen
| Phase | Ziel | Aktion |
|---|---|---|
| C0 | Vorbereitung | Tests, Images, Manifests, Secrets vorbereiten |
| C1 | Shadow Deployment | OpenShift-Service ohne produktiven Traffic deployen |
| C2 | Read-only Checks | Health, DB, SOAP, REST, Rollen prüfen |
| C3 | SOAP Paralleltest | SOAP Requests gegen Legacy und Neu vergleichen |
| C4 | Partner Pilot | ausgewählte Partner auf neue Route leiten |
| C5 | Backoffice Pilot | ausgewählte interne Nutzer umstellen |
| C6 | Event Cutover | Outbox/MQ produktiv aktivieren |
| C7 | Batch Cutover | EJB Timer deaktivieren, CronJobs aktivieren |
| C8 | Vollbetrieb | Traffic vollständig auf OpenShift |
| C9 | Legacy Freeze | WebSphere nur noch Fallback/Read-only |
Rollback-Plan
Rollback ist möglich, solange:
- Legacy-Datenbankmodell kompatibel bleibt,
- SOAP-Verträge kompatibel bleiben,
- Events idempotent sind,
- neue Outbox-Events nicht unkontrolliert doppelt verarbeitet werden,
- Batchjobs nicht gleichzeitig in Legacy und OpenShift laufen.
Rollback Plan
#### ROLLBACK_PLAN
### Sofort-Rollback
1. OpenShift Route zurück auf Legacy Gateway zeigen lassen.
2. Neue CronJobs suspendieren:
oc patch cronjob claim-sla-escalation -p '{"spec":{"suspend":true}}'
3. Outbox Publisher pausieren.
4. WebSphere TimerBeans wieder aktivieren, falls sie deaktiviert wurden.
5. Monitoring auf doppelte Events prüfen.
6. Datenbankstatus mit Golden-Master-Vergleich prüfen.
### Nicht erlaubt
- OpenShift Batch und WebSphere Timer gleichzeitig aktiv lassen.
- Payment Release ohne Idempotency aktivieren.
- Partner Portal ohne SOAP-Kompatibilität umleiten.Migration Decisions
#### MIGRATION_DECISIONS.md
### MD-001: Keine Big-Bang-Migration
Entscheidung:
Die Migration erfolgt schrittweise mit Strangler-Ansatz.
Warum:
Das Legacy-System hat viele externe Abhängigkeiten und Fachrisiken.
Nutzen:
Geringeres Ausfallrisiko, bessere Testbarkeit, parallele Vergleichbarkeit.
Risiko:
Übergangsphase ist länger und braucht saubere Schnittstellen.
### MD-002: SOAP bleibt zunächst kompatibel
Entscheidung:
SOAP-Endpunkte werden in der Zielruntime weiterhin angeboten.
Warum:
Partner, Backoffice und externe Systeme können nicht gleichzeitig migriert werden.
Nutzen:
Technische Migration wird von Client-Migration entkoppelt.
Risiko:
SOAP-Kompatibilität erzeugt Zusatzaufwand.
### MD-003: EJB Timer wird durch OpenShift CronJob ersetzt
Entscheidung:
SLA/Eskalationslogik wird in claim-batch verschoben.
Warum:
EJB Timer ist WebSphere-gebunden und schwer zu skalieren.
Nutzen:
Bessere Betriebssteuerung, klare Job-Historie, bessere Testbarkeit.
Risiko:
Doppelte Ausführung muss durch Lease Lock verhindert werden.
### MD-004: Outbox statt direkter JMS-Publizierung
Entscheidung:
Fachliche Events werden zuerst in CLAIM_OUTBOX persistiert.
Warum:
DB-Änderung und Event-Publikation müssen konsistent sein.
Nutzen:
Retry, Idempotency, Auditierbarkeit.
Risiko:
Outbox Publisher muss überwacht werden.
### MD-005: FileShare nur als Übergangsadapter
Entscheidung:
FileShareDocumentAdapter bleibt für Migration erhalten.
Warum:
Dokumentenbestand kann nicht sofort in Object Storage migriert werden.
Nutzen:
Schrittweise Ablösung möglich.
Risiko:
RWX Storage und Netzwerkzugriff müssen stabil betrieben werden.Risiken
#### RISKS.md
### R-001: Unvollständige SOAP-Kompatibilität
Auswirkung:
Partner oder Legacy-Systeme können nicht migrieren.
Gegenmaßnahme:
SOAP Contract Tests, WSDL-Vergleich, Pilotpartner.
### R-002: Doppelte Zahlungen
Auswirkung:
Finanzieller Schaden.
Gegenmaßnahme:
Payment Idempotency Key, PaymentReleasePort, Vergleichstest, Outbox.
### R-003: Doppelte Eskalationen
Auswirkung:
Falsche Benachrichtigungen, Prozesschaos.
Gegenmaßnahme:
CronJob concurrencyPolicy Forbid, Batch Lease Lock, idempotente Statusänderung.
### R-004: FileShare nicht containerfähig
Auswirkung:
Dokumente nicht erreichbar.
Gegenmaßnahme:
DocumentStoragePort, PVC/RWX Übergang, Object-Storage-Zielbild.
### R-005: Security-Rollen falsch gemappt
Auswirkung:
Unberechtigte Bearbeitung oder blockierte Nutzer.
Gegenmaßnahme:
RoleMapping Tests, Pilotgruppen, Audit Events.
### R-006: Stored Procedure Seiteneffekte vergessen
Auswirkung:
Fachliche Abweichungen.
Gegenmaßnahme:
Golden Master Tests, DB-Diff, Reporting-Vergleich.Before/After-Migrationsmapping Runde 5
| Legacy | OpenShift-Ziel | Grund | Sicherheitsnetz |
|---|---|---|---|
| WebSphere EAR | claim-service Deployment | Runtime entkoppeln | Deployment Smoke Test |
| EJB Timer | OpenShift CronJob | Scheduler portabel machen | Batch Lease Lock Test |
| direkte JMS-Publizierung | Outbox + Publisher CronJob | Transaktionssicherheit | OutboxPublisherIT |
| JNDI DataSource | Config/Secret/DataSource Config | reproduzierbare Konfiguration | OracleConnectivityIT |
| JAAS SessionContext | SecurityContextPort | Security abstrahieren | RoleMappingIT |
| FileShare Hardcoding | DocumentStoragePort + PVC/Object Storage | Dokumentenablage austauschbar | DocumentAdapterIT |
| WebSphere Console Config | YAML/ConfigMap/Secret | GitOps-fähig | Structure Check |
| technische Logs | JSON Logs + Audit Events | Observability | Log Sampling Check |
| manuelles Deployment | OpenShift Manifests/Pipeline | Wiederholbarkeit | Pipeline Smoke Test |
Changelog-V2-Vorbereitung für Runde 5
Neue Changelog-Kandidaten:
| ID | Kategorie | Änderung |
|---|---|---|
| CHG-OS-001 | Runtime | WebSphere EAR in OpenShift Deployment überführt |
| CHG-OS-002 | Container | Open Liberty Containerfile ergänzt |
| CHG-OS-003 | Container | Spring Boot Alternative ergänzt |
| CHG-OS-004 | Config | WebSphere Console Config durch ConfigMap/Secret ersetzt |
| CHG-OS-005 | Health | Liveness/Readiness/Startup Probes eingeführt |
| CHG-OS-006 | Batch | EJB Timer durch OpenShift CronJob ersetzt |
| CHG-OS-007 | Messaging | direkte JMS-Publizierung durch Outbox Publisher ersetzt |
| CHG-OS-008 | Security | JAAS-Rollenmapping in AuthorizationPolicy überführt |
| CHG-OS-009 | Storage | FileShare-Strategie containerfähig gemacht |
| CHG-OS-010 | Observability | Logging/Metrics/Tracing ergänzt |
| CHG-OS-011 | Network | NetworkPolicy für externe Systeme vorbereitet |
| CHG-OS-012 | Tests | OpenShift Migration Tests ergänzt |
| CHG-OS-013 | Rollback | Cutover- und Rollback-Plan ergänzt |
Zusammenfassung Runde 5
Diese Runde hat die Migration von der WebSphere-Traditional-Landschaft nach OpenShift konkretisiert:
- Containerfiles für Open Liberty und Spring Boot,
- OpenShift Namespace, ServiceAccount, ConfigMap, Secret,
- Deployment, Service, Route,
- Health Checks,
- Externalized Configuration,
- Oracle-Migrationsstrategie,
- JMS/Outbox-Migration,
- SLA/Escalation CronJob,
- Batch Lease Lock,
- Document Storage Migration,
- Security Migration,
- NetworkPolicy,
- HPA und PDB,
- Observability,
- Migrationstests,
- Cutover- und Rollback-Plan,
- Migration Decisions,
- Risikoübersicht,
- Before/After-Mapping-Vorbereitung,
- Changelog-V2-Vorbereitung.
077. Lernbuch-Artefakte und Ausgabeformate
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Lernbuch-Artefakte und Ausgabeformate in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Ziel dieser Runde
Diese Runde erzeugt die ersten lesbaren Lernbuch-Artefakte aus dem bisherigen Rohmaterial:
- konsolidiertes Markdown-Lernbuch
masterbook.md - PC-HTML-Version
pc.html - Mobile/iPhone-Safari-taugliche HTML-Version
mobile.html - PC-PDF
pc.pdf - Mobile-PDF
mobile.pdf - SVG-/HTML-/PDF-Prüfnotizen
Die HTML-Versionen halten die iPhone-/Safari-Regeln ein:
- keine externen Bibliotheken
- keine CDN-Links
- keine externen Bilder
- Navigation über normale Ankerlinks
- wichtige Inhalte ohne JavaScript erreichbar
- Lösungen und Zusatzbereiche über
details/summary - SVG-Grafiken direkt inline eingebettet
- keine Pflicht-Buttons
- Zurück/Weiter als echte Links
Konsolidierte Lernbuch-Struktur
- Projektüberblick und Zielbild
- Legacy-Systemlandschaft
- Zusatzsysteme und Integrationsgrenzen
- Legacy-Ausgangssystem vor Refactoring
- Monster Method
processClaimDecision(...) - Refactoring-Safety-Net
- Schrittweise Zerlegung der Monster Method
- Moderne modulare Maven-Zielarchitektur
- Ports & Adapters / Hexagonale Architektur
- SOAP-Kompatibilität und REST/OpenAPI-Erweiterung
- OpenShift-Migration
- Migrationstests, Risiken und Rollback
- Artefakt- und Qualitätsstrategie
- Glossar und Checklisten
Didaktische Gestaltung
Die PC-Version ist als großes Lernbuch mit Sidebar gedacht. Die Mobile-Version verwendet dieselben Inhalte, aber ein schmaleres Layout, größere Touchflächen und einfache Navigation. Beide HTML-Dateien bleiben offline nutzbar.
SVG-Grafiken in dieser Runde
Eingebettet wurden erste robuste Inline-SVGs für:
- Gesamt-Systemlandschaft
- Legacy-Architektur vor Refactoring
- Monster Method Zerlegung
- Ports & Adapters / Hexagonale Architektur
- Workflow / State Machine
- Outbox / Messaging Flow
- Security-Migration
- OpenShift Zielarchitektur
- Before/After-Mapping-Grafik
- Refactoring Roadmap
Beispiel: stabile HTML-Lösung ohne Pflicht-JavaScript
<details>
<summary>Analyse anzeigen</summary>
<p>Die Fachlogik ist direkt mit EJB, SOAP, JDBC, JMS und File Shares gekoppelt.</p>
</details>Beispiel: echte Navigation statt JavaScript-Button
<nav>
<a href="#zielbild">Zielbild</a>
<a href="#legacy">Legacy-System</a>
<a href="#refactoring">Refactoring</a>
</nav>Beispiel: Codeblock für PDF/HTML kompakt halten
package com.seb4u.demo.claims.application;
public final class ProcessClaimDecisionHandler {
private final ClaimRepository claimRepository;
private final ClaimTransitionPolicy transitionPolicy;
private final DocumentVerificationPort documentVerificationPort;
private final FraudRiskPort fraudRiskPort;
private final CoveragePort coveragePort;
private final PaymentReleasePort paymentReleasePort;
private final AuditLogPort auditLogPort;
public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
Claim claim = claimRepository.getRequired(command.claimId());
transitionPolicy.assertAllowed(claim.status(), command.decisionType());
documentVerificationPort.verifyRequiredDocuments(command.claimId());
fraudRiskPort.assessRisk(command.claimId());
coveragePort.checkCoverage(command.claimId());
paymentReleasePort.preparePaymentIfRequired(command.claimId(), command.decisionType());
auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
return ProcessClaimDecisionResult.accepted(command.claimId());
}
}088. Änderungsdokumentation und Mapping
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Änderungsdokumentation und Mapping in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Erzeugt am: 2026-07-05T11:05:00+02:00
Diese Runde ergänzt die bisherige Lernprojekt-Arbeit um die Änderungsdokumentation. Sie macht sichtbar, welche Legacy-Dateien, technischen Schulden und Kopplungen in welche modernen Module, Ports, Policies, Handler und Adapter überführt werden.
Ergebnis dieser Runde
- Refactoring-Phasen-Paket definiert
- Essential-Changes-Paket definiert
- CHANGELOG_V2 in MD, Kurzfassung, CSV und JSON erzeugt
- BEFORE_AFTER_MAPPING in MD, Kurzfassung, CSV und JSON erzeugt
- Migration Decisions und Risks ergänzt
- zentrale Codebeispiele für Runde 7 erzeugt
Refactoring-Phasen
| Phase | Titel | Ziel | Technik/Änderung | Sicherheitsnetz | Risiko |
|---|---|---|---|---|---|
| 01 | Inventory und Characterization | Systeme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisieren | Keine Änderung am Produktionscode | Inventory-Dokument, Golden-Master-Snapshot | Falsche Systemannahmen |
| 02 | Systemlandschaft und Zusatzsysteme erfassen | Partner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreiben | Kontextgrenzen sichtbar | System Landscape Map | verdeckte Kopplungen |
| 03 | Monster Method verstehen | processClaimDecision(...) absichern und fachliche Schritte markieren | Seams, Logging, Testdaten | Characterization Test | Tests bilden Bugs mit ab |
| 04 | Facade und Command extrahieren | Input in Command Object und Result Object überführen | Command Handler | Handler Test | Transaktionskontext prüfen |
| 05 | Statuslogik in State/Policy überführen | if/else durch ClaimTransitionPolicy ersetzen | State Pattern, Policy Object | State Transition Test | Sonderstatus fehlen |
| 06 | Coverage/Fraud/Document/Payment Ports extrahieren | direkte SOAP/File/Payment Calls ablösen | Ports & Adapters | Adapter Contract Tests | Fehlersemantik angleichen |
| 07 | Customer und Identity Ports extrahieren | Stammdaten und Rollenprüfung isolieren | ACL, AuthorizationPolicy | Role Mapping Test | Rollenmigration |
| 08 | Workflow Orchestrator einführen | Ablauf koordinieren, Fachlogik in Use Cases halten | Workflow Orchestrator | Workflow Scenario Test | God-Orchestrator vermeiden |
| 09 | Audit Trail abstrahieren | fachliche Audit Events statt technischer Logs | AuditLogPort | Audit Snapshot Test | PII/Compliance |
| 10 | Search, Notification, Reporting trennen | seiteneffektarme Boundaries schaffen | Read Model, Outbox | Search/Notification Tests | Eventual consistency |
| 11 | Outbox und Idempotency einführen | JMS/Payment sicher wiederholbar machen | Outbox Pattern, Idempotency Key | Replay/Idempotency Tests | Doppelte externe Effekte |
| 12 | Batch und SLA trennen | TimerBeans in Batch Worker und Policy überführen | Batch Worker, Lease Lock | Batch Lease Test | Doppelte Jobs |
| 13 | Portal und Backoffice entkoppeln | UI über REST/BFF schrittweise strangeln | Strangler Fig | Contract Tests | parallele UI-Pfade |
| 14 | OpenShift-Migration vorbereiten | Runtime Boundary, Config, Health, Security, Observability | Container, ConfigMap, Secret, CronJob | Deployment Smoke Test | Runtime-Unterschiede |
Essential Changes
| ID | Änderung | Vorher | Nachher | Pattern / Konzept | Relevante Ordner | Test / Sicherheitsnetz | Migrationsnutzen |
|---|---|---|---|---|---|---|---|
| CLG-001 | Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht | Nur Zielarchitektur diskutiert | /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar | Architecture Baseline | docs, legacy-websphere-ear | Characterization Tests | Migration startet mit belegbarem Ist-Zustand |
| CLG-002 | Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen | EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik | claim-domain und claim-application enthalten Geschäftsregeln | Hexagonal Architecture | claim-domain, claim-application | Unit Tests, ArchUnit | Fachlogik ohne WebSphere testbar |
| CLG-003 | processClaimDecision(...) in fachliche Schritte zerlegt | Eine große Methode entscheidet, prüft, speichert, publiziert | Command Handler + Workflow Orchestrator + Ports | Command Handler, Orchestrator | claim-application, claim-workflow | Golden Master, Handler Tests | Schrittweise Migration möglich |
| CLG-004 | Claim Statuslogik aus if/else-Ketten herausgezogen | Statusübergänge in verschachtelten if/else | ClaimState + ClaimTransitionPolicy | State Pattern, Policy Object | claim-domain | State Transition Tests | Regeln werden sichtbar und änderbar |
| CLG-005 | Workflow Orchestrator eingeführt | Workflow versteckt im EJB | ClaimDecisionWorkflowOrchestrator koordiniert Schritte | Workflow Orchestrator | claim-workflow | Workflow Scenario Tests | Ablauf kann später als Service laufen |
| CLG-006 | Ports & Adapters eingeführt | Direkte SOAP/JMS/JDBC/File-Aufrufe | Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter | Ports & Adapters | claim-application, claim-infrastructure | Adapter Contract Tests | Technik austauschbar |
| CLG-007 | SOAP Boundary kompatibel gehalten | Partner hängen direkt an Legacy SOAP Endpoint | claim-soap-api übersetzt SOAP auf Use Cases | Facade, Anti-Corruption Layer | claim-soap-api | SOAP Compatibility Tests | Partner müssen nicht sofort migrieren |
| CLG-008 | REST/OpenAPI ergänzt | Keine moderne API für neue UIs | claim-rest-api + claim-openapi | API Facade | claim-rest-api, claim-openapi | REST Smoke Tests | BFF/Frontend-Strangler möglich |
| CLG-009 | Document Adapter eingeführt | File Shares hart verdrahtet | DocumentStoragePort + FileShareDocumentAdapter | Adapter, ACL | claim-document | Adapter Tests | später Object Storage möglich |
| CLG-010 | Fraud Risk Gateway entkoppelt | Direkter SOAP Client im Fachfluss | FraudRiskPort + FraudRiskSoapAdapter | Anti-Corruption Layer | claim-infrastructure | Contract Test, Timeout Test | Fehler-/Retry-Politik steuerbar |
| CLG-011 | Policy Coverage über CoveragePort abstrahiert | Coverage SOAP/Stored Procedure direkt genutzt | CoveragePort + CoverageDecision | Specification, Policy Object | claim-domain, claim-infrastructure | Coverage Tests | Alte Tariflogik isoliert |
| CLG-012 | Payment Release entkoppelt | Zahlung direkt in JTA-Fluss ausgelöst | PaymentReleasePort + Outbox/Compensation | Outbox, Idempotency | claim-payment | Idempotency Tests | Zahlungen sicherer migrierbar |
| CLG-013 | NotificationPort modernisiert | EJB sendet JMS/SOAP/E-Mail direkt | NotificationPort + NotificationAdapter | Adapter, Outbox | claim-notification | DLQ/Retry Tests | Benachrichtigung entkoppelt |
| CLG-014 | SLA & Escalation Scheduler getrennt | EJB Timer mit DB-Seiteneffekten | EscalationBatchJob + EscalationPolicy | Batch Worker, Policy Object | claim-batch | Batch Lease Tests | CronJob auf OpenShift möglich |
| CLG-015 | Audit & Compliance Boundary getrennt | Technische Logs und Stored Procedures vermischt | AuditLogPort + Reporting Read Model | Audit Event, Read Model | claim-audit | Audit Snapshot Tests | Compliance nachvollziehbar |
| CLG-016 | Search Indexer über ClaimSearchPort abstrahiert | Oracle LIKE Queries in DAO/Backoffice | ClaimSearchPort + SearchIndexAdapter | Read Model, Adapter | claim-search | Search Adapter Tests | später OpenSearch möglich |
| CLG-017 | Customer Master Data entkoppelt | Direkter SOAP Client und alte Kundennummern | CustomerMasterDataPort + CustomerSnapshot | ACL, Value Object | claim-infrastructure | Customer Contract Tests | Dubletten/Inkonsistenz isoliert |
| CLG-018 | Identity & Role Management abstrahiert | JAAS/WebSphere-Rollen hart kodiert | SecurityContextPort + AuthorizationPolicy | Policy Object, Role Mapping | claim-application, claim-infrastructure | Role Mapping Tests | IdP-Migration vorbereitet |
| CLG-019 | Outbox und Idempotency eingeführt | JMS/Payment im gleichen Fluss ohne Wiederholschutz | OutboxEvent + IdempotencyKey | Outbox Pattern | claim-infrastructure, claim-payment | Outbox Publisher Tests | robuster bei Pod-Restarts |
| CLG-020 | OpenShift-/Runtime-Migration vorbereitet | EAR manuell auf WebSphere deployt | Container, Deployment, Route, ConfigMap, Secret | Runtime Boundary | migration_to_openshift | Deployment Smoke Tests | Automatisierbare Migration |
Changelog V2
| ID | Kategorie | Änderung | Vorher | Nachher | Pattern / Konzept | Module | Tests | Migrationsnutzen | Risiko |
|---|---|---|---|---|---|---|---|---|---|
| CLG-001 | Legacy sichtbar gemacht | Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht | Nur Zielarchitektur diskutiert | /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar | Architecture Baseline | docs, legacy-websphere-ear | Characterization Tests | Migration startet mit belegbarem Ist-Zustand | Alte Annahmen können falsch sein |
| CLG-002 | Fachlogik | Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen | EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik | claim-domain und claim-application enthalten Geschäftsregeln | Hexagonal Architecture | claim-domain, claim-application | Unit Tests, ArchUnit | Fachlogik ohne WebSphere testbar | Transaktionsgrenzen neu bewerten |
| CLG-003 | Monster Method | processClaimDecision(...) in fachliche Schritte zerlegt | Eine große Methode entscheidet, prüft, speichert, publiziert | Command Handler + Workflow Orchestrator + Ports | Command Handler, Orchestrator | claim-application, claim-workflow | Golden Master, Handler Tests | Schrittweise Migration möglich | Verhalten muss identisch bleiben |
| CLG-004 | Statuslogik | Claim Statuslogik aus if/else-Ketten herausgezogen | Statusübergänge in verschachtelten if/else | ClaimState + ClaimTransitionPolicy | State Pattern, Policy Object | claim-domain | State Transition Tests | Regeln werden sichtbar und änderbar | Sonderfälle übersehen |
| CLG-005 | Workflow | Workflow Orchestrator eingeführt | Workflow versteckt im EJB | ClaimDecisionWorkflowOrchestrator koordiniert Schritte | Workflow Orchestrator | claim-workflow | Workflow Scenario Tests | Ablauf kann später als Service laufen | Zu viel Logik im Orchestrator vermeiden |
| CLG-006 | Ports | Ports & Adapters eingeführt | Direkte SOAP/JMS/JDBC/File-Aufrufe | Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter | Ports & Adapters | claim-application, claim-infrastructure | Adapter Contract Tests | Technik austauschbar | Port-Schnittstellen nicht zu technisch machen |
| CLG-007 | SOAP | SOAP Boundary kompatibel gehalten | Partner hängen direkt an Legacy SOAP Endpoint | claim-soap-api übersetzt SOAP auf Use Cases | Facade, Anti-Corruption Layer | claim-soap-api | SOAP Compatibility Tests | Partner müssen nicht sofort migrieren | Schema-Kompatibilität prüfen |
| CLG-008 | REST | REST/OpenAPI ergänzt | Keine moderne API für neue UIs | claim-rest-api + claim-openapi | API Facade | claim-rest-api, claim-openapi | REST Smoke Tests | BFF/Frontend-Strangler möglich | Doppelte API-Semantik vermeiden |
| CLG-009 | Document | Document Adapter eingeführt | File Shares hart verdrahtet | DocumentStoragePort + FileShareDocumentAdapter | Adapter, ACL | claim-document | Adapter Tests | später Object Storage möglich | Dateipfade/Permissions |
| CLG-010 | Fraud | Fraud Risk Gateway entkoppelt | Direkter SOAP Client im Fachfluss | FraudRiskPort + FraudRiskSoapAdapter | Anti-Corruption Layer | claim-infrastructure | Contract Test, Timeout Test | Fehler-/Retry-Politik steuerbar | Fachliche vs technische Fehler trennen |
| CLG-011 | Coverage | Policy Coverage über CoveragePort abstrahiert | Coverage SOAP/Stored Procedure direkt genutzt | CoveragePort + CoverageDecision | Specification, Policy Object | claim-domain, claim-infrastructure | Coverage Tests | Alte Tariflogik isoliert | Regelabweichungen dokumentieren |
| CLG-012 | Payment | Payment Release entkoppelt | Zahlung direkt in JTA-Fluss ausgelöst | PaymentReleasePort + Outbox/Compensation | Outbox, Idempotency | claim-payment | Idempotency Tests | Zahlungen sicherer migrierbar | Doppelauszahlung vermeiden |
| CLG-013 | Notification | NotificationPort modernisiert | EJB sendet JMS/SOAP/E-Mail direkt | NotificationPort + NotificationAdapter | Adapter, Outbox | claim-notification | DLQ/Retry Tests | Benachrichtigung entkoppelt | Templates versionieren |
| CLG-014 | SLA | SLA & Escalation Scheduler getrennt | EJB Timer mit DB-Seiteneffekten | EscalationBatchJob + EscalationPolicy | Batch Worker, Policy Object | claim-batch | Batch Lease Tests | CronJob auf OpenShift möglich | Doppelverarbeitung verhindern |
| CLG-015 | Audit | Audit & Compliance Boundary getrennt | Technische Logs und Stored Procedures vermischt | AuditLogPort + Reporting Read Model | Audit Event, Read Model | claim-audit | Audit Snapshot Tests | Compliance nachvollziehbar | Datenschutz/PII beachten |
| CLG-016 | Search | Search Indexer über ClaimSearchPort abstrahiert | Oracle LIKE Queries in DAO/Backoffice | ClaimSearchPort + SearchIndexAdapter | Read Model, Adapter | claim-search | Search Adapter Tests | später OpenSearch möglich | Indexkonsistenz |
| CLG-017 | Customer | Customer Master Data entkoppelt | Direkter SOAP Client und alte Kundennummern | CustomerMasterDataPort + CustomerSnapshot | ACL, Value Object | claim-infrastructure | Customer Contract Tests | Dubletten/Inkonsistenz isoliert | Datenqualität bleibt Thema |
| CLG-018 | Identity | Identity & Role Management abstrahiert | JAAS/WebSphere-Rollen hart kodiert | SecurityContextPort + AuthorizationPolicy | Policy Object, Role Mapping | claim-application, claim-infrastructure | Role Mapping Tests | IdP-Migration vorbereitet | Rollenmatrix abgleichen |
| CLG-019 | Outbox | Outbox und Idempotency eingeführt | JMS/Payment im gleichen Fluss ohne Wiederholschutz | OutboxEvent + IdempotencyKey | Outbox Pattern | claim-infrastructure, claim-payment | Outbox Publisher Tests | robuster bei Pod-Restarts | Eventual Consistency erklären |
| CLG-020 | OpenShift | OpenShift-/Runtime-Migration vorbereitet | EAR manuell auf WebSphere deployt | Container, Deployment, Route, ConfigMap, Secret | Runtime Boundary | migration_to_openshift | Deployment Smoke Tests | Automatisierbare Migration | Runtime-Unterschiede prüfen |
Before/After-Mapping
| Legacy-Datei / Modul | Neue Datei / neues Modul | Grund | Pattern / Architekturkonzept | Test / Sicherheitsnetz | Migrationsnutzen |
|---|---|---|---|---|---|
| LegacyClaimFacadeBean.java | ProcessClaimDecisionHandler.java | Monster Method zerlegt | Command Handler, Application Service | Golden Master + Handler Test | Fachlogik kann unabhängig von WebSphere getestet und migriert werden |
| LegacyClaimStatusIfElse.java | ClaimState.java + ClaimTransitionPolicy.java | Statuslogik aus if/else-Ketten herausgezogen | State Pattern, Policy Object | State Transition Test | Workflow-Regeln werden verständlich und testbar |
| LegacyDocumentShareClient.java | DocumentStoragePort.java + FileShareDocumentAdapter.java | File Share entkoppelt | Port, Adapter, Anti-Corruption Layer | Adapter Test | Dokumentenablage kann später ersetzt werden |
| LegacyPartnerPortalServlet.java | PartnerPortalRestController.java / PartnerPortalBffAdapter.java | Partner Portal vom EJB getrennt | Strangler UI, BFF Adapter | Portal Contract Test | Externes Portal kann schrittweise modernisiert werden |
| InternalClaimsBackofficeBean.java | ClaimBackofficeUseCase.java | Backoffice-Logik aus JSP/EJB herausgezogen | Use Case, Application Service | Backoffice Use Case Test | Interne UI kann später Angular/React nutzen |
| FraudRiskSoapClient.java | FraudRiskPort.java + FraudRiskSoapAdapter.java | Fraud SOAP entkoppelt | Port, Adapter, ACL | Fraud Contract Test | Timeout/Retry/Circuit Breaker möglich |
| PolicyCoverageSoapClient.java | CoveragePort.java + CoverageSoapAdapter.java | Coverage-System isoliert | Specification, Policy Object, Adapter | Coverage Decision Test | Alte Tariflogik kontrolliert migrierbar |
| PaymentReleaseSoapClient.java | PaymentReleasePort.java + PaymentReleaseSoapAdapter.java | Payment-Freigabe entkoppelt | Port, Adapter, Idempotency | Payment Idempotency Test | Zahlungsrisiken werden reduziert |
| LegacyNotificationSenderBean.java | NotificationPort.java + NotificationAdapter.java | Benachrichtigung entkoppelt | Outbox, Adapter | Notification Retry Test | DLQ/Retry auf Plattformebene möglich |
| SlaEscalationTimerBean.java | EscalationBatchJob.java + EscalationPolicy.java | EJB Timer ersetzt | Batch Worker, Policy Object, Lease Lock | Batch Lease Test | OpenShift CronJob möglich |
| AuditReportStoredProcedureDao.java | AuditLogPort.java + AuditReportingAdapter.java | Audit/Reporting getrennt | Audit Event, Reporting Read Model | Audit Trail Test | Compliance-Berichte werden fachlich nachvollziehbar |
| LegacyClaimSearchDao.java | ClaimSearchPort.java + SearchIndexAdapter.java | DB-LIKE-Suche entkoppelt | Read Model, Adapter | Search Adapter Test | später Elasticsearch/OpenSearch möglich |
| CustomerMasterDataSoapClient.java | CustomerMasterDataPort.java + CustomerMasterDataSoapAdapter.java | Customer-Stammdaten entkoppelt | ACL, Snapshot Value Object | Customer Contract Test | Datenqualitätsprobleme werden isoliert |
| JaasRoleChecker.java | SecurityContextPort.java + AuthorizationPolicy.java | Rollenprüfung zentralisiert | Policy Object, Role Mapping | Security Role Mapping Test | OpenShift/IdP-Migration vorbereitet |
| LegacyClaimEventPublisher.java | OutboxEvent.java + OutboxPublisherWorker.java | Direkte JMS-Publizierung abgelöst | Outbox Pattern | Outbox Replay Test | robust gegen Transaktions- und Pod-Fehler |
| LegacyClaimDao.java | ClaimRepository.java + JpaClaimRepository.java | DAO und Fachmodell getrennt | Repository, Data Mapper | Repository Integration Test | DB-Zugriff ist austauschbar |
| ClaimDecisionSoapEndpoint.java | ClaimDecisionSoapEndpoint.java + ProcessClaimDecisionHandler.java | SOAP bleibt, delegiert aber nur | Facade, ACL | SOAP Compatibility IT | Partner bleiben kompatibel |
| LegacyAuditLogger.java | AuditLogPort.java + StructuredAuditLogAdapter.java | versteckte Auditlogik sichtbar gemacht | Port, Structured Logging | Audit Snapshot Test | Audit kann zentral ausgewertet werden |
Codebeispiele
Zentrale Codebeispiele
State/Policy statt if/else
package com.seb4u.demo.claims.domain.workflow;
public final class ClaimTransitionPolicy {
public void assertAllowed(ClaimStatus current, ClaimDecisionType decision) {
if (current == ClaimStatus.CLOSED) {
throw new ClaimTransitionNotAllowedException("Closed claims cannot be changed");
}
if (decision == ClaimDecisionType.APPROVE_PAYMENT && current != ClaimStatus.DOCUMENTS_VERIFIED) {
throw new ClaimTransitionNotAllowedException("Payment approval requires verified documents");
}
}
}Command Handler als neue Use-Case-Grenze
package com.seb4u.demo.claims.application.decision;
public class ProcessClaimDecisionHandler {
private final ClaimRepository repository;
private final ClaimTransitionPolicy transitionPolicy;
private final DocumentVerificationPort documentVerificationPort;
private final FraudRiskPort fraudRiskPort;
private final CoveragePort coveragePort;
private final PaymentReleasePort paymentReleasePort;
private final AuditLogPort auditLogPort;
public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
Claim claim = repository.getRequired(command.claimId());
transitionPolicy.assertAllowed(claim.status(), command.decisionType());
var documentDecision = documentVerificationPort.verify(command.claimId(), command.documentIds());
var risk = fraudRiskPort.calculateRisk(command.claimId(), claim.customerId());
var coverage = coveragePort.checkCoverage(claim.policyId(), command.claimId());
claim.applyDecision(command.decisionType(), documentDecision, risk, coverage);
repository.save(claim);
auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
return ProcessClaimDecisionResult.from(claim);
}
}Mapping-Test gegen Legacy-Verhalten
package com.seb4u.demo.claims.migrationtests;
class LegacyVsModularDecisionComparisonTest {
@Test
void approvePayment_keepsLegacyVisibleOutcome() {
LegacyDecisionSnapshot legacy = legacyRunner.run("CLAIM-1001", "APPROVE_PAYMENT");
ProcessClaimDecisionResult modern = modernHandler.handle(commandFor("CLAIM-1001", APPROVE_PAYMENT));
assertThat(modern.status()).isEqualTo(legacy.status());
assertThat(modern.auditCodes()).containsExactlyElementsOf(legacy.auditCodes());
}
}099. Anhang A - Refactoring Phases
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang A - Refactoring Phases in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
REFACTORING_PHASES
Refactoring-Phasen-Paket für das Legacy Claims & Customer Support Enterprise System.
| Phase | Titel | Ziel | Technik/Änderung | Sicherheitsnetz | Risiko |
|---|---|---|---|---|---|
| 01 | Inventory und Characterization | Systeme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisieren | Keine Änderung am Produktionscode | Inventory-Dokument, Golden-Master-Snapshot | Falsche Systemannahmen |
| 02 | Systemlandschaft und Zusatzsysteme erfassen | Partner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreiben | Kontextgrenzen sichtbar | System Landscape Map | verdeckte Kopplungen |
| 03 | Monster Method verstehen | processClaimDecision(...) absichern und fachliche Schritte markieren | Seams, Logging, Testdaten | Characterization Test | Tests bilden Bugs mit ab |
| 04 | Facade und Command extrahieren | Input in Command Object und Result Object überführen | Command Handler | Handler Test | Transaktionskontext prüfen |
| 05 | Statuslogik in State/Policy überführen | if/else durch ClaimTransitionPolicy ersetzen | State Pattern, Policy Object | State Transition Test | Sonderstatus fehlen |
| 06 | Coverage/Fraud/Document/Payment Ports extrahieren | direkte SOAP/File/Payment Calls ablösen | Ports & Adapters | Adapter Contract Tests | Fehlersemantik angleichen |
| 07 | Customer und Identity Ports extrahieren | Stammdaten und Rollenprüfung isolieren | ACL, AuthorizationPolicy | Role Mapping Test | Rollenmigration |
| 08 | Workflow Orchestrator einführen | Ablauf koordinieren, Fachlogik in Use Cases halten | Workflow Orchestrator | Workflow Scenario Test | God-Orchestrator vermeiden |
| 09 | Audit Trail abstrahieren | fachliche Audit Events statt technischer Logs | AuditLogPort | Audit Snapshot Test | PII/Compliance |
| 10 | Search, Notification, Reporting trennen | seiteneffektarme Boundaries schaffen | Read Model, Outbox | Search/Notification Tests | Eventual consistency |
| 11 | Outbox und Idempotency einführen | JMS/Payment sicher wiederholbar machen | Outbox Pattern, Idempotency Key | Replay/Idempotency Tests | Doppelte externe Effekte |
| 12 | Batch und SLA trennen | TimerBeans in Batch Worker und Policy überführen | Batch Worker, Lease Lock | Batch Lease Test | Doppelte Jobs |
| 13 | Portal und Backoffice entkoppeln | UI über REST/BFF schrittweise strangeln | Strangler Fig | Contract Tests | parallele UI-Pfade |
| 14 | OpenShift-Migration vorbereiten | Runtime Boundary, Config, Health, Security, Observability | Container, ConfigMap, Secret, CronJob | Deployment Smoke Test | Runtime-Unterschiede |
Phasenordner
01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/Phase 01 – Inventory und Characterization
Ziel: Systeme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisieren
Änderung: Keine Änderung am Produktionscode
Sicherheitsnetz: Inventory-Dokument, Golden-Master-Snapshot
Risiko: Falsche Systemannahmen
Phase 02 – Systemlandschaft und Zusatzsysteme erfassen
Ziel: Partner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreiben
Änderung: Kontextgrenzen sichtbar
Sicherheitsnetz: System Landscape Map
Risiko: verdeckte Kopplungen
Phase 03 – Monster Method verstehen
Ziel: processClaimDecision(...) absichern und fachliche Schritte markieren
Änderung: Seams, Logging, Testdaten
Sicherheitsnetz: Characterization Test
Risiko: Tests bilden Bugs mit ab
Phase 04 – Facade und Command extrahieren
Ziel: Input in Command Object und Result Object überführen
Änderung: Command Handler
Sicherheitsnetz: Handler Test
Risiko: Transaktionskontext prüfen
Phase 05 – Statuslogik in State/Policy überführen
Ziel: if/else durch ClaimTransitionPolicy ersetzen
Änderung: State Pattern, Policy Object
Sicherheitsnetz: State Transition Test
Risiko: Sonderstatus fehlen
Phase 06 – Coverage/Fraud/Document/Payment Ports extrahieren
Ziel: direkte SOAP/File/Payment Calls ablösen
Änderung: Ports & Adapters
Sicherheitsnetz: Adapter Contract Tests
Risiko: Fehlersemantik angleichen
Phase 07 – Customer und Identity Ports extrahieren
Ziel: Stammdaten und Rollenprüfung isolieren
Änderung: ACL, AuthorizationPolicy
Sicherheitsnetz: Role Mapping Test
Risiko: Rollenmigration
Phase 08 – Workflow Orchestrator einführen
Ziel: Ablauf koordinieren, Fachlogik in Use Cases halten
Änderung: Workflow Orchestrator
Sicherheitsnetz: Workflow Scenario Test
Risiko: God-Orchestrator vermeiden
Phase 09 – Audit Trail abstrahieren
Ziel: fachliche Audit Events statt technischer Logs
Änderung: AuditLogPort
Sicherheitsnetz: Audit Snapshot Test
Risiko: PII/Compliance
Phase 10 – Search, Notification, Reporting trennen
Ziel: seiteneffektarme Boundaries schaffen
Änderung: Read Model, Outbox
Sicherheitsnetz: Search/Notification Tests
Risiko: Eventual consistency
Phase 11 – Outbox und Idempotency einführen
Ziel: JMS/Payment sicher wiederholbar machen
Änderung: Outbox Pattern, Idempotency Key
Sicherheitsnetz: Replay/Idempotency Tests
Risiko: Doppelte externe Effekte
Phase 12 – Batch und SLA trennen
Ziel: TimerBeans in Batch Worker und Policy überführen
Änderung: Batch Worker, Lease Lock
Sicherheitsnetz: Batch Lease Test
Risiko: Doppelte Jobs
Phase 13 – Portal und Backoffice entkoppeln
Ziel: UI über REST/BFF schrittweise strangeln
Änderung: Strangler Fig
Sicherheitsnetz: Contract Tests
Risiko: parallele UI-Pfade
Phase 14 – OpenShift-Migration vorbereiten
Ziel: Runtime Boundary, Config, Health, Security, Observability
Änderung: Container, ConfigMap, Secret, CronJob
Sicherheitsnetz: Deployment Smoke Test
Risiko: Runtime-Unterschiede
1010. Anhang B - Essential Changes
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang B - Essential Changes in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
ESSENTIAL CHANGES – Change Catalog
Dieses Paket erklärt die wichtigsten Änderungen jeweils mit Vorher/Nachher, Pattern, Ordnern, Tests und Migrationsnutzen.
| ID | Änderung | Vorher | Nachher | Pattern / Konzept | Relevante Ordner | Test / Sicherheitsnetz | Migrationsnutzen |
|---|---|---|---|---|---|---|---|
| CLG-001 | Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht | Nur Zielarchitektur diskutiert | /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar | Architecture Baseline | docs, legacy-websphere-ear | Characterization Tests | Migration startet mit belegbarem Ist-Zustand |
| CLG-002 | Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen | EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik | claim-domain und claim-application enthalten Geschäftsregeln | Hexagonal Architecture | claim-domain, claim-application | Unit Tests, ArchUnit | Fachlogik ohne WebSphere testbar |
| CLG-003 | processClaimDecision(...) in fachliche Schritte zerlegt | Eine große Methode entscheidet, prüft, speichert, publiziert | Command Handler + Workflow Orchestrator + Ports | Command Handler, Orchestrator | claim-application, claim-workflow | Golden Master, Handler Tests | Schrittweise Migration möglich |
| CLG-004 | Claim Statuslogik aus if/else-Ketten herausgezogen | Statusübergänge in verschachtelten if/else | ClaimState + ClaimTransitionPolicy | State Pattern, Policy Object | claim-domain | State Transition Tests | Regeln werden sichtbar und änderbar |
| CLG-005 | Workflow Orchestrator eingeführt | Workflow versteckt im EJB | ClaimDecisionWorkflowOrchestrator koordiniert Schritte | Workflow Orchestrator | claim-workflow | Workflow Scenario Tests | Ablauf kann später als Service laufen |
| CLG-006 | Ports & Adapters eingeführt | Direkte SOAP/JMS/JDBC/File-Aufrufe | Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter | Ports & Adapters | claim-application, claim-infrastructure | Adapter Contract Tests | Technik austauschbar |
| CLG-007 | SOAP Boundary kompatibel gehalten | Partner hängen direkt an Legacy SOAP Endpoint | claim-soap-api übersetzt SOAP auf Use Cases | Facade, Anti-Corruption Layer | claim-soap-api | SOAP Compatibility Tests | Partner müssen nicht sofort migrieren |
| CLG-008 | REST/OpenAPI ergänzt | Keine moderne API für neue UIs | claim-rest-api + claim-openapi | API Facade | claim-rest-api, claim-openapi | REST Smoke Tests | BFF/Frontend-Strangler möglich |
| CLG-009 | Document Adapter eingeführt | File Shares hart verdrahtet | DocumentStoragePort + FileShareDocumentAdapter | Adapter, ACL | claim-document | Adapter Tests | später Object Storage möglich |
| CLG-010 | Fraud Risk Gateway entkoppelt | Direkter SOAP Client im Fachfluss | FraudRiskPort + FraudRiskSoapAdapter | Anti-Corruption Layer | claim-infrastructure | Contract Test, Timeout Test | Fehler-/Retry-Politik steuerbar |
| CLG-011 | Policy Coverage über CoveragePort abstrahiert | Coverage SOAP/Stored Procedure direkt genutzt | CoveragePort + CoverageDecision | Specification, Policy Object | claim-domain, claim-infrastructure | Coverage Tests | Alte Tariflogik isoliert |
| CLG-012 | Payment Release entkoppelt | Zahlung direkt in JTA-Fluss ausgelöst | PaymentReleasePort + Outbox/Compensation | Outbox, Idempotency | claim-payment | Idempotency Tests | Zahlungen sicherer migrierbar |
| CLG-013 | NotificationPort modernisiert | EJB sendet JMS/SOAP/E-Mail direkt | NotificationPort + NotificationAdapter | Adapter, Outbox | claim-notification | DLQ/Retry Tests | Benachrichtigung entkoppelt |
| CLG-014 | SLA & Escalation Scheduler getrennt | EJB Timer mit DB-Seiteneffekten | EscalationBatchJob + EscalationPolicy | Batch Worker, Policy Object | claim-batch | Batch Lease Tests | CronJob auf OpenShift möglich |
| CLG-015 | Audit & Compliance Boundary getrennt | Technische Logs und Stored Procedures vermischt | AuditLogPort + Reporting Read Model | Audit Event, Read Model | claim-audit | Audit Snapshot Tests | Compliance nachvollziehbar |
| CLG-016 | Search Indexer über ClaimSearchPort abstrahiert | Oracle LIKE Queries in DAO/Backoffice | ClaimSearchPort + SearchIndexAdapter | Read Model, Adapter | claim-search | Search Adapter Tests | später OpenSearch möglich |
| CLG-017 | Customer Master Data entkoppelt | Direkter SOAP Client und alte Kundennummern | CustomerMasterDataPort + CustomerSnapshot | ACL, Value Object | claim-infrastructure | Customer Contract Tests | Dubletten/Inkonsistenz isoliert |
| CLG-018 | Identity & Role Management abstrahiert | JAAS/WebSphere-Rollen hart kodiert | SecurityContextPort + AuthorizationPolicy | Policy Object, Role Mapping | claim-application, claim-infrastructure | Role Mapping Tests | IdP-Migration vorbereitet |
| CLG-019 | Outbox und Idempotency eingeführt | JMS/Payment im gleichen Fluss ohne Wiederholschutz | OutboxEvent + IdempotencyKey | Outbox Pattern | claim-infrastructure, claim-payment | Outbox Publisher Tests | robuster bei Pod-Restarts |
| CLG-020 | OpenShift-/Runtime-Migration vorbereitet | EAR manuell auf WebSphere deployt | Container, Deployment, Route, ConfigMap, Secret | Runtime Boundary | migration_to_openshift | Deployment Smoke Tests | Automatisierbare Migration |
1111. Anhang C - Changelog V2
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang C - Changelog V2 in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
CHANGELOG V2 – Legacy Claims & Customer Support Enterprise System
Dieses Dokument sammelt die wesentlichen Architektur-, Refactoring- und Migrationsänderungen der Runde 7.
| ID | Kategorie | Änderung | Vorher | Nachher | Pattern / Konzept | Module | Tests | Migrationsnutzen | Risiko |
|---|---|---|---|---|---|---|---|---|---|
| CLG-001 | Legacy sichtbar gemacht | Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht | Nur Zielarchitektur diskutiert | /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar | Architecture Baseline | docs, legacy-websphere-ear | Characterization Tests | Migration startet mit belegbarem Ist-Zustand | Alte Annahmen können falsch sein |
| CLG-002 | Fachlogik | Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen | EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik | claim-domain und claim-application enthalten Geschäftsregeln | Hexagonal Architecture | claim-domain, claim-application | Unit Tests, ArchUnit | Fachlogik ohne WebSphere testbar | Transaktionsgrenzen neu bewerten |
| CLG-003 | Monster Method | processClaimDecision(...) in fachliche Schritte zerlegt | Eine große Methode entscheidet, prüft, speichert, publiziert | Command Handler + Workflow Orchestrator + Ports | Command Handler, Orchestrator | claim-application, claim-workflow | Golden Master, Handler Tests | Schrittweise Migration möglich | Verhalten muss identisch bleiben |
| CLG-004 | Statuslogik | Claim Statuslogik aus if/else-Ketten herausgezogen | Statusübergänge in verschachtelten if/else | ClaimState + ClaimTransitionPolicy | State Pattern, Policy Object | claim-domain | State Transition Tests | Regeln werden sichtbar und änderbar | Sonderfälle übersehen |
| CLG-005 | Workflow | Workflow Orchestrator eingeführt | Workflow versteckt im EJB | ClaimDecisionWorkflowOrchestrator koordiniert Schritte | Workflow Orchestrator | claim-workflow | Workflow Scenario Tests | Ablauf kann später als Service laufen | Zu viel Logik im Orchestrator vermeiden |
| CLG-006 | Ports | Ports & Adapters eingeführt | Direkte SOAP/JMS/JDBC/File-Aufrufe | Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter | Ports & Adapters | claim-application, claim-infrastructure | Adapter Contract Tests | Technik austauschbar | Port-Schnittstellen nicht zu technisch machen |
| CLG-007 | SOAP | SOAP Boundary kompatibel gehalten | Partner hängen direkt an Legacy SOAP Endpoint | claim-soap-api übersetzt SOAP auf Use Cases | Facade, Anti-Corruption Layer | claim-soap-api | SOAP Compatibility Tests | Partner müssen nicht sofort migrieren | Schema-Kompatibilität prüfen |
| CLG-008 | REST | REST/OpenAPI ergänzt | Keine moderne API für neue UIs | claim-rest-api + claim-openapi | API Facade | claim-rest-api, claim-openapi | REST Smoke Tests | BFF/Frontend-Strangler möglich | Doppelte API-Semantik vermeiden |
| CLG-009 | Document | Document Adapter eingeführt | File Shares hart verdrahtet | DocumentStoragePort + FileShareDocumentAdapter | Adapter, ACL | claim-document | Adapter Tests | später Object Storage möglich | Dateipfade/Permissions |
| CLG-010 | Fraud | Fraud Risk Gateway entkoppelt | Direkter SOAP Client im Fachfluss | FraudRiskPort + FraudRiskSoapAdapter | Anti-Corruption Layer | claim-infrastructure | Contract Test, Timeout Test | Fehler-/Retry-Politik steuerbar | Fachliche vs technische Fehler trennen |
| CLG-011 | Coverage | Policy Coverage über CoveragePort abstrahiert | Coverage SOAP/Stored Procedure direkt genutzt | CoveragePort + CoverageDecision | Specification, Policy Object | claim-domain, claim-infrastructure | Coverage Tests | Alte Tariflogik isoliert | Regelabweichungen dokumentieren |
| CLG-012 | Payment | Payment Release entkoppelt | Zahlung direkt in JTA-Fluss ausgelöst | PaymentReleasePort + Outbox/Compensation | Outbox, Idempotency | claim-payment | Idempotency Tests | Zahlungen sicherer migrierbar | Doppelauszahlung vermeiden |
| CLG-013 | Notification | NotificationPort modernisiert | EJB sendet JMS/SOAP/E-Mail direkt | NotificationPort + NotificationAdapter | Adapter, Outbox | claim-notification | DLQ/Retry Tests | Benachrichtigung entkoppelt | Templates versionieren |
| CLG-014 | SLA | SLA & Escalation Scheduler getrennt | EJB Timer mit DB-Seiteneffekten | EscalationBatchJob + EscalationPolicy | Batch Worker, Policy Object | claim-batch | Batch Lease Tests | CronJob auf OpenShift möglich | Doppelverarbeitung verhindern |
| CLG-015 | Audit | Audit & Compliance Boundary getrennt | Technische Logs und Stored Procedures vermischt | AuditLogPort + Reporting Read Model | Audit Event, Read Model | claim-audit | Audit Snapshot Tests | Compliance nachvollziehbar | Datenschutz/PII beachten |
| CLG-016 | Search | Search Indexer über ClaimSearchPort abstrahiert | Oracle LIKE Queries in DAO/Backoffice | ClaimSearchPort + SearchIndexAdapter | Read Model, Adapter | claim-search | Search Adapter Tests | später OpenSearch möglich | Indexkonsistenz |
| CLG-017 | Customer | Customer Master Data entkoppelt | Direkter SOAP Client und alte Kundennummern | CustomerMasterDataPort + CustomerSnapshot | ACL, Value Object | claim-infrastructure | Customer Contract Tests | Dubletten/Inkonsistenz isoliert | Datenqualität bleibt Thema |
| CLG-018 | Identity | Identity & Role Management abstrahiert | JAAS/WebSphere-Rollen hart kodiert | SecurityContextPort + AuthorizationPolicy | Policy Object, Role Mapping | claim-application, claim-infrastructure | Role Mapping Tests | IdP-Migration vorbereitet | Rollenmatrix abgleichen |
| CLG-019 | Outbox | Outbox und Idempotency eingeführt | JMS/Payment im gleichen Fluss ohne Wiederholschutz | OutboxEvent + IdempotencyKey | Outbox Pattern | claim-infrastructure, claim-payment | Outbox Publisher Tests | robuster bei Pod-Restarts | Eventual Consistency erklären |
| CLG-020 | OpenShift | OpenShift-/Runtime-Migration vorbereitet | EAR manuell auf WebSphere deployt | Container, Deployment, Route, ConfigMap, Secret | Runtime Boundary | migration_to_openshift | Deployment Smoke Tests | Automatisierbare Migration | Runtime-Unterschiede prüfen |
Ausführliche Hinweise
CLG-001 – Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht
- Vorher: Nur Zielarchitektur diskutiert
- Nachher: /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar
- Konzept: Architecture Baseline
- Betroffene Module: docs, legacy-websphere-ear
- Sicherheitsnetz: Characterization Tests
- Migrationsnutzen: Migration startet mit belegbarem Ist-Zustand
- Risiko / Achtung: Alte Annahmen können falsch sein
CLG-002 – Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen
- Vorher: EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik
- Nachher: claim-domain und claim-application enthalten Geschäftsregeln
- Konzept: Hexagonal Architecture
- Betroffene Module: claim-domain, claim-application
- Sicherheitsnetz: Unit Tests, ArchUnit
- Migrationsnutzen: Fachlogik ohne WebSphere testbar
- Risiko / Achtung: Transaktionsgrenzen neu bewerten
CLG-003 – processClaimDecision(...) in fachliche Schritte zerlegt
- Vorher: Eine große Methode entscheidet, prüft, speichert, publiziert
- Nachher: Command Handler + Workflow Orchestrator + Ports
- Konzept: Command Handler, Orchestrator
- Betroffene Module: claim-application, claim-workflow
- Sicherheitsnetz: Golden Master, Handler Tests
- Migrationsnutzen: Schrittweise Migration möglich
- Risiko / Achtung: Verhalten muss identisch bleiben
CLG-004 – Claim Statuslogik aus if/else-Ketten herausgezogen
- Vorher: Statusübergänge in verschachtelten if/else
- Nachher: ClaimState + ClaimTransitionPolicy
- Konzept: State Pattern, Policy Object
- Betroffene Module: claim-domain
- Sicherheitsnetz: State Transition Tests
- Migrationsnutzen: Regeln werden sichtbar und änderbar
- Risiko / Achtung: Sonderfälle übersehen
CLG-005 – Workflow Orchestrator eingeführt
- Vorher: Workflow versteckt im EJB
- Nachher: ClaimDecisionWorkflowOrchestrator koordiniert Schritte
- Konzept: Workflow Orchestrator
- Betroffene Module: claim-workflow
- Sicherheitsnetz: Workflow Scenario Tests
- Migrationsnutzen: Ablauf kann später als Service laufen
- Risiko / Achtung: Zu viel Logik im Orchestrator vermeiden
CLG-006 – Ports & Adapters eingeführt
- Vorher: Direkte SOAP/JMS/JDBC/File-Aufrufe
- Nachher: Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter
- Konzept: Ports & Adapters
- Betroffene Module: claim-application, claim-infrastructure
- Sicherheitsnetz: Adapter Contract Tests
- Migrationsnutzen: Technik austauschbar
- Risiko / Achtung: Port-Schnittstellen nicht zu technisch machen
CLG-007 – SOAP Boundary kompatibel gehalten
- Vorher: Partner hängen direkt an Legacy SOAP Endpoint
- Nachher: claim-soap-api übersetzt SOAP auf Use Cases
- Konzept: Facade, Anti-Corruption Layer
- Betroffene Module: claim-soap-api
- Sicherheitsnetz: SOAP Compatibility Tests
- Migrationsnutzen: Partner müssen nicht sofort migrieren
- Risiko / Achtung: Schema-Kompatibilität prüfen
CLG-008 – REST/OpenAPI ergänzt
- Vorher: Keine moderne API für neue UIs
- Nachher: claim-rest-api + claim-openapi
- Konzept: API Facade
- Betroffene Module: claim-rest-api, claim-openapi
- Sicherheitsnetz: REST Smoke Tests
- Migrationsnutzen: BFF/Frontend-Strangler möglich
- Risiko / Achtung: Doppelte API-Semantik vermeiden
CLG-009 – Document Adapter eingeführt
- Vorher: File Shares hart verdrahtet
- Nachher: DocumentStoragePort + FileShareDocumentAdapter
- Konzept: Adapter, ACL
- Betroffene Module: claim-document
- Sicherheitsnetz: Adapter Tests
- Migrationsnutzen: später Object Storage möglich
- Risiko / Achtung: Dateipfade/Permissions
CLG-010 – Fraud Risk Gateway entkoppelt
- Vorher: Direkter SOAP Client im Fachfluss
- Nachher: FraudRiskPort + FraudRiskSoapAdapter
- Konzept: Anti-Corruption Layer
- Betroffene Module: claim-infrastructure
- Sicherheitsnetz: Contract Test, Timeout Test
- Migrationsnutzen: Fehler-/Retry-Politik steuerbar
- Risiko / Achtung: Fachliche vs technische Fehler trennen
CLG-011 – Policy Coverage über CoveragePort abstrahiert
- Vorher: Coverage SOAP/Stored Procedure direkt genutzt
- Nachher: CoveragePort + CoverageDecision
- Konzept: Specification, Policy Object
- Betroffene Module: claim-domain, claim-infrastructure
- Sicherheitsnetz: Coverage Tests
- Migrationsnutzen: Alte Tariflogik isoliert
- Risiko / Achtung: Regelabweichungen dokumentieren
CLG-012 – Payment Release entkoppelt
- Vorher: Zahlung direkt in JTA-Fluss ausgelöst
- Nachher: PaymentReleasePort + Outbox/Compensation
- Konzept: Outbox, Idempotency
- Betroffene Module: claim-payment
- Sicherheitsnetz: Idempotency Tests
- Migrationsnutzen: Zahlungen sicherer migrierbar
- Risiko / Achtung: Doppelauszahlung vermeiden
CLG-013 – NotificationPort modernisiert
- Vorher: EJB sendet JMS/SOAP/E-Mail direkt
- Nachher: NotificationPort + NotificationAdapter
- Konzept: Adapter, Outbox
- Betroffene Module: claim-notification
- Sicherheitsnetz: DLQ/Retry Tests
- Migrationsnutzen: Benachrichtigung entkoppelt
- Risiko / Achtung: Templates versionieren
CLG-014 – SLA & Escalation Scheduler getrennt
- Vorher: EJB Timer mit DB-Seiteneffekten
- Nachher: EscalationBatchJob + EscalationPolicy
- Konzept: Batch Worker, Policy Object
- Betroffene Module: claim-batch
- Sicherheitsnetz: Batch Lease Tests
- Migrationsnutzen: CronJob auf OpenShift möglich
- Risiko / Achtung: Doppelverarbeitung verhindern
CLG-015 – Audit & Compliance Boundary getrennt
- Vorher: Technische Logs und Stored Procedures vermischt
- Nachher: AuditLogPort + Reporting Read Model
- Konzept: Audit Event, Read Model
- Betroffene Module: claim-audit
- Sicherheitsnetz: Audit Snapshot Tests
- Migrationsnutzen: Compliance nachvollziehbar
- Risiko / Achtung: Datenschutz/PII beachten
CLG-016 – Search Indexer über ClaimSearchPort abstrahiert
- Vorher: Oracle LIKE Queries in DAO/Backoffice
- Nachher: ClaimSearchPort + SearchIndexAdapter
- Konzept: Read Model, Adapter
- Betroffene Module: claim-search
- Sicherheitsnetz: Search Adapter Tests
- Migrationsnutzen: später OpenSearch möglich
- Risiko / Achtung: Indexkonsistenz
CLG-017 – Customer Master Data entkoppelt
- Vorher: Direkter SOAP Client und alte Kundennummern
- Nachher: CustomerMasterDataPort + CustomerSnapshot
- Konzept: ACL, Value Object
- Betroffene Module: claim-infrastructure
- Sicherheitsnetz: Customer Contract Tests
- Migrationsnutzen: Dubletten/Inkonsistenz isoliert
- Risiko / Achtung: Datenqualität bleibt Thema
CLG-018 – Identity & Role Management abstrahiert
- Vorher: JAAS/WebSphere-Rollen hart kodiert
- Nachher: SecurityContextPort + AuthorizationPolicy
- Konzept: Policy Object, Role Mapping
- Betroffene Module: claim-application, claim-infrastructure
- Sicherheitsnetz: Role Mapping Tests
- Migrationsnutzen: IdP-Migration vorbereitet
- Risiko / Achtung: Rollenmatrix abgleichen
CLG-019 – Outbox und Idempotency eingeführt
- Vorher: JMS/Payment im gleichen Fluss ohne Wiederholschutz
- Nachher: OutboxEvent + IdempotencyKey
- Konzept: Outbox Pattern
- Betroffene Module: claim-infrastructure, claim-payment
- Sicherheitsnetz: Outbox Publisher Tests
- Migrationsnutzen: robuster bei Pod-Restarts
- Risiko / Achtung: Eventual Consistency erklären
CLG-020 – OpenShift-/Runtime-Migration vorbereitet
- Vorher: EAR manuell auf WebSphere deployt
- Nachher: Container, Deployment, Route, ConfigMap, Secret
- Konzept: Runtime Boundary
- Betroffene Module: migration_to_openshift
- Sicherheitsnetz: Deployment Smoke Tests
- Migrationsnutzen: Automatisierbare Migration
- Risiko / Achtung: Runtime-Unterschiede prüfen
1212. Anhang D - Before/After Mapping
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang D - Before/After Mapping in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
BEFORE/AFTER-MAPPING
Mapping von Legacy-Dateien und Legacy-Modulen auf moderne Dateien, Module und Konzepte.
| Legacy-Datei / Modul | Neue Datei / neues Modul | Grund | Pattern / Architekturkonzept | Test / Sicherheitsnetz | Migrationsnutzen |
|---|---|---|---|---|---|
| LegacyClaimFacadeBean.java | ProcessClaimDecisionHandler.java | Monster Method zerlegt | Command Handler, Application Service | Golden Master + Handler Test | Fachlogik kann unabhängig von WebSphere getestet und migriert werden |
| LegacyClaimStatusIfElse.java | ClaimState.java + ClaimTransitionPolicy.java | Statuslogik aus if/else-Ketten herausgezogen | State Pattern, Policy Object | State Transition Test | Workflow-Regeln werden verständlich und testbar |
| LegacyDocumentShareClient.java | DocumentStoragePort.java + FileShareDocumentAdapter.java | File Share entkoppelt | Port, Adapter, Anti-Corruption Layer | Adapter Test | Dokumentenablage kann später ersetzt werden |
| LegacyPartnerPortalServlet.java | PartnerPortalRestController.java / PartnerPortalBffAdapter.java | Partner Portal vom EJB getrennt | Strangler UI, BFF Adapter | Portal Contract Test | Externes Portal kann schrittweise modernisiert werden |
| InternalClaimsBackofficeBean.java | ClaimBackofficeUseCase.java | Backoffice-Logik aus JSP/EJB herausgezogen | Use Case, Application Service | Backoffice Use Case Test | Interne UI kann später Angular/React nutzen |
| FraudRiskSoapClient.java | FraudRiskPort.java + FraudRiskSoapAdapter.java | Fraud SOAP entkoppelt | Port, Adapter, ACL | Fraud Contract Test | Timeout/Retry/Circuit Breaker möglich |
| PolicyCoverageSoapClient.java | CoveragePort.java + CoverageSoapAdapter.java | Coverage-System isoliert | Specification, Policy Object, Adapter | Coverage Decision Test | Alte Tariflogik kontrolliert migrierbar |
| PaymentReleaseSoapClient.java | PaymentReleasePort.java + PaymentReleaseSoapAdapter.java | Payment-Freigabe entkoppelt | Port, Adapter, Idempotency | Payment Idempotency Test | Zahlungsrisiken werden reduziert |
| LegacyNotificationSenderBean.java | NotificationPort.java + NotificationAdapter.java | Benachrichtigung entkoppelt | Outbox, Adapter | Notification Retry Test | DLQ/Retry auf Plattformebene möglich |
| SlaEscalationTimerBean.java | EscalationBatchJob.java + EscalationPolicy.java | EJB Timer ersetzt | Batch Worker, Policy Object, Lease Lock | Batch Lease Test | OpenShift CronJob möglich |
| AuditReportStoredProcedureDao.java | AuditLogPort.java + AuditReportingAdapter.java | Audit/Reporting getrennt | Audit Event, Reporting Read Model | Audit Trail Test | Compliance-Berichte werden fachlich nachvollziehbar |
| LegacyClaimSearchDao.java | ClaimSearchPort.java + SearchIndexAdapter.java | DB-LIKE-Suche entkoppelt | Read Model, Adapter | Search Adapter Test | später Elasticsearch/OpenSearch möglich |
| CustomerMasterDataSoapClient.java | CustomerMasterDataPort.java + CustomerMasterDataSoapAdapter.java | Customer-Stammdaten entkoppelt | ACL, Snapshot Value Object | Customer Contract Test | Datenqualitätsprobleme werden isoliert |
| JaasRoleChecker.java | SecurityContextPort.java + AuthorizationPolicy.java | Rollenprüfung zentralisiert | Policy Object, Role Mapping | Security Role Mapping Test | OpenShift/IdP-Migration vorbereitet |
| LegacyClaimEventPublisher.java | OutboxEvent.java + OutboxPublisherWorker.java | Direkte JMS-Publizierung abgelöst | Outbox Pattern | Outbox Replay Test | robust gegen Transaktions- und Pod-Fehler |
| LegacyClaimDao.java | ClaimRepository.java + JpaClaimRepository.java | DAO und Fachmodell getrennt | Repository, Data Mapper | Repository Integration Test | DB-Zugriff ist austauschbar |
| ClaimDecisionSoapEndpoint.java | ClaimDecisionSoapEndpoint.java + ProcessClaimDecisionHandler.java | SOAP bleibt, delegiert aber nur | Facade, ACL | SOAP Compatibility IT | Partner bleiben kompatibel |
| LegacyAuditLogger.java | AuditLogPort.java + StructuredAuditLogAdapter.java | versteckte Auditlogik sichtbar gemacht | Port, Structured Logging | Audit Snapshot Test | Audit kann zentral ausgewertet werden |
Didaktische Lesart
Das Mapping ist nicht nur eine Datei-Verschiebung. Jede Zeile zeigt eine Architekturentscheidung: Fachlogik wird aus technischen Schichten herausgelöst, technische Adapter werden austauschbar und Migration wird testbar.
1313. Anhang E - Migration Decisions
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang E - Migration Decisions in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
MIGRATION_DECISIONS
- WebSphere Traditional wird nicht 1:1 containerisiert, sondern über Runtime Boundaries schrittweise ersetzt.
- SOAP bleibt als Kompatibilitätsgrenze erhalten.
- REST/OpenAPI wird als neue Erweiterungsgrenze ergänzt.
- JNDI-Konfiguration wird durch externe Konfiguration via ConfigMap/Secret ersetzt.
- EJB Timer werden zu OpenShift CronJobs mit Lease Lock.
- JMS-Seiteneffekte werden über Outbox und Idempotency abgesichert.
- FileShare bleibt zunächst Adapter, später Object Storage möglich.
- JAAS/LDAP wird über SecurityContextPort und AuthorizationPolicy gekapselt.
1414. Anhang F - Risks
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang F - Risks in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
RISKS
| Risiko | Beschreibung | Gegenmaßnahme |
|---|---|---|
| Verhaltensänderung | Refactoring verändert unbemerkt Legacy-Verhalten | Golden Master und Characterization Tests |
| Doppelte Zahlungen | Payment-Retry löst extern mehrfach aus | IdempotencyKey, Outbox, Contract Tests |
| Rollenfehler | JAAS-Gruppen werden falsch gemappt | Role Mapping Test, Vier-Augen-Prüfung |
| Batch-Duplikate | mehrere Pods starten denselben Job | Lease Lock, CronJob Policy |
| Datenqualität | Customer Master Data bleibt inkonsistent | CustomerSnapshot, ACL, Qualitätsreports |
1515. Anhang G - Runde 07 Codebeispiele
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang G - Runde 07 Codebeispiele in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Runde 7 – zentrale Codebeispiele
State/Policy statt if/else
package com.seb4u.demo.claims.domain.workflow;
public final class ClaimTransitionPolicy {
public void assertAllowed(ClaimStatus current, ClaimDecisionType decision) {
if (current == ClaimStatus.CLOSED) {
throw new ClaimTransitionNotAllowedException("Closed claims cannot be changed");
}
if (decision == ClaimDecisionType.APPROVE_PAYMENT && current != ClaimStatus.DOCUMENTS_VERIFIED) {
throw new ClaimTransitionNotAllowedException("Payment approval requires verified documents");
}
}
}Command Handler als neue Use-Case-Grenze
package com.seb4u.demo.claims.application.decision;
public class ProcessClaimDecisionHandler {
private final ClaimRepository repository;
private final ClaimTransitionPolicy transitionPolicy;
private final DocumentVerificationPort documentVerificationPort;
private final FraudRiskPort fraudRiskPort;
private final CoveragePort coveragePort;
private final PaymentReleasePort paymentReleasePort;
private final AuditLogPort auditLogPort;
public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
Claim claim = repository.getRequired(command.claimId());
transitionPolicy.assertAllowed(claim.status(), command.decisionType());
var documentDecision = documentVerificationPort.verify(command.claimId(), command.documentIds());
var risk = fraudRiskPort.calculateRisk(command.claimId(), claim.customerId());
var coverage = coveragePort.checkCoverage(claim.policyId(), command.claimId());
claim.applyDecision(command.decisionType(), documentDecision, risk, coverage);
repository.save(claim);
auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
return ProcessClaimDecisionResult.from(claim);
}
}Mapping-Test gegen Legacy-Verhalten
package com.seb4u.demo.claims.migrationtests;
class LegacyVsModularDecisionComparisonTest {
@Test
void approvePayment_keepsLegacyVisibleOutcome() {
LegacyDecisionSnapshot legacy = legacyRunner.run("CLAIM-1001", "APPROVE_PAYMENT");
ProcessClaimDecisionResult modern = modernHandler.handle(commandFor("CLAIM-1001", APPROVE_PAYMENT));
assertThat(modern.status()).isEqualTo(legacy.status());
assertThat(modern.auditCodes()).containsExactlyElementsOf(legacy.auditCodes());
}
}1616. Anhang H - Großes Glossar
Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang H - Großes Glossar in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.
Dieses Glossar ist bewusst als großer Nachschlage-Anhang aufgebaut. Es erklärt zentrale Begriffe aus Legacy-Analyse, Refactoring, moderner Zielarchitektur, Zusatzsystemen, OpenShift-Migration und Claims-Fachlichkeit. Die Einträge sind nicht als Ersatz für die Kapitel gedacht, sondern als schnelle Orientierung beim Lesen des Lernbuchs.
Hinweis: Die Begriffe sind in Kategorien gruppiert. Unterpunkte innerhalb des Anhangs sind keine eigenen Klappkapitel, sondern normale Abschnitte innerhalb des Glossars.
Legacy-Analyse und Ausgangssystem
Legacy-System
Erklärung: Gewachsene Anwendung mit historischer Architektur, alten Laufzeiten, manuellen Abläufen und oft unklaren Verantwortlichkeiten.
Beispiel im Lernprojekt: WebSphere Traditional mit EAR, WAR, EJB, SOAP, JMS, Oracle und File Shares.
Warum wichtig: Macht sichtbar, was zuerst abgesichert und verstanden werden muss.
IBM WebSphere Traditional
Erklärung: Klassische Java-Enterprise-Laufzeit mit serverseitiger Konfiguration, JNDI, Deployment-Deskriptoren und EAR-Deployments.
Beispiel im Lernprojekt: Ausgangspunkt der Migration.
Warum wichtig: Trennung von Laufzeitabhängigkeiten und Fachlogik.
EAR
Erklärung: Enterprise Archive, das mehrere Java-EE-Module wie WAR und EJB-JAR gemeinsam deployt.
Beispiel im Lernprojekt: legacy-websphere-ear.
Warum wichtig: Module können später getrennt gebaut, getestet und deployt werden.
WAR
Erklärung: Web Archive für Weboberflächen, Servlets, JSPs und webnahe Ressourcen.
Beispiel im Lernprojekt: Partner Portal und Internal Backoffice.
Warum wichtig: UI-Schicht von Fachlogik trennen.
EJB-JAR
Erklärung: Archiv für Enterprise JavaBeans, häufig mit Transaktionen, Security, Scheduler und Fachlogik vermischt.
Beispiel im Lernprojekt: LegacyClaimFacadeBean.
Warum wichtig: Use Cases aus Container-Code herausziehen.
EJB
Erklärung: Enterprise JavaBean. Im Legacy-System oft Ort von Transaktion, Integration, Security und Fachentscheidung zugleich.
Beispiel im Lernprojekt: processClaimDecision in einem Session Bean.
Warum wichtig: Fachlogik wird testbar, wenn sie in Application Services wandert.
JSP
Erklärung: Serverseitige View-Technologie, bei der HTML und Java-nahe Logik häufig vermischt werden.
Beispiel im Lernprojekt: Backoffice-Masken und Partnerportal-Seiten.
Warum wichtig: UI kann schrittweise durch REST/BFF abgelöst werden.
Servlet
Erklärung: Java-Webkomponente für HTTP-Requests, im Legacy oft direkt mit EJBs oder SOAP-Clients gekoppelt.
Beispiel im Lernprojekt: LegacyPartnerPortalServlet.
Warum wichtig: Controller werden dünn, Use Cases werden separat.
SOAP
Erklärung: XML-basiertes Webservice-Protokoll mit festen Contracts und WSDLs.
Beispiel im Lernprojekt: createClaim, getClaimStatus, processClaimDecision.
Warum wichtig: Kompatibilität bleibt erhalten, intern wird modernisiert.
WSDL
Erklärung: Beschreibung eines SOAP-Service mit Operationen, Nachrichten und Datentypen.
Beispiel im Lernprojekt: Claims SOAP API.
Warum wichtig: Contract Tests schützen bestehende Partner.
JMS
Erklärung: Java Message Service für asynchrone Nachrichten und Events.
Beispiel im Lernprojekt: ClaimDecisionProcessedEvent an IBM MQ.
Warum wichtig: Outbox entkoppelt Eventversand von Fachtransaktion.
IBM MQ
Erklärung: Enterprise-Messaging-System, das häufig mit JMS integriert wird.
Beispiel im Lernprojekt: Legacy-Queues für Notifications, Audit oder Payment.
Warum wichtig: Adapter und Outbox reduzieren direkte Kopplung.
JTA
Erklärung: Java Transaction API für containerverwaltete Transaktionen.
Beispiel im Lernprojekt: Eine Methode schreibt Daten, sendet JMS und ruft SOAP auf.
Warum wichtig: Transaktionsgrenzen werden bewusst gemacht.
JNDI
Erklärung: Namensdienst für DataSources, Queues und Ressourcen in Application Servern.
Beispiel im Lernprojekt: java:comp/env/jdbc/ClaimsDS.
Warum wichtig: Externe Konfiguration ersetzt servergebundene Lookups.
JNDI DataSource
Erklärung: Serverseitig konfigurierte Datenbankverbindung.
Beispiel im Lernprojekt: Oracle DataSource in WebSphere.
Warum wichtig: Konfiguration wird container- und OpenShift-tauglich.
JAAS
Erklärung: Java Authentication and Authorization Service für klassische Rollen- und Sicherheitsprüfung.
Beispiel im Lernprojekt: JaasRoleChecker.
Warum wichtig: SecurityContextPort und AuthorizationPolicy ersetzen harte Prüfungen.
LDAP
Erklärung: Verzeichnisdienst für Benutzer, Gruppen und Rollen.
Beispiel im Lernprojekt: Claims-Agent, Teamlead, Partnergruppe.
Warum wichtig: Rollenmapping wird dokumentiert und testbar.
Stored Procedure
Erklärung: Datenbankprozedur, die Fachlogik, Reporting oder technische Aktionen kapseln kann.
Beispiel im Lernprojekt: SP_PROCESS_CLAIM_DECISION.
Warum wichtig: Fachlogik wird aus der Datenbank heraus identifiziert und isoliert.
Oracle LIKE Query
Erklärung: Einfache textuelle Suche mit SQL-LIKE, oft langsam und unscharf.
Beispiel im Lernprojekt: LegacyClaimSearchDao.
Warum wichtig: SearchPort ermöglicht später Search Read Model oder OpenSearch.
File Share
Erklärung: Gemeinsame Dateifreigabe für Dokumente, häufig mit harten Pfaden und schwacher Fehlerbehandlung.
Beispiel im Lernprojekt: LegacyDocumentShareClient.
Warum wichtig: DocumentStoragePort kapselt die Ablage.
Refactoring-Grundlagen
Refactoring
Erklärung: Strukturelle Verbesserung von Code, ohne das beobachtbare Verhalten absichtlich zu ändern.
Beispiel im Lernprojekt: Zerlegung von processClaimDecision.
Warum wichtig: Risikoarme Modernisierung vor Plattformwechsel.
Characterization Test
Erklärung: Test, der aktuelles Legacy-Verhalten festhält, auch wenn dieses Verhalten nicht ideal ist.
Beispiel im Lernprojekt: LegacyProcessClaimDecisionCharacterizationTest.
Warum wichtig: Sicherheitsnetz vor dem Umbau.
Golden Master Test
Erklärung: Vergleicht große Ein-/Ausgabe-Snapshots, um Verhalten über Refactoring-Schritte hinweg zu sichern.
Beispiel im Lernprojekt: GoldenMasterSnapshot.
Warum wichtig: Erkennt unbeabsichtigte Verhaltensänderungen.
Refactoring Seam
Erklärung: Kontrollierter Schnittpunkt, an dem Legacy-Code abgefangen, getestet oder schrittweise umgeleitet werden kann.
Beispiel im Lernprojekt: Wrapper um LegacyClaimFacadeBean.
Warum wichtig: Erlaubt inkrementelle Änderungen.
Monster Method
Erklärung: Sehr große Methode mit vielen Verantwortlichkeiten, Bedingungen, Seiteneffekten und Integrationen.
Beispiel im Lernprojekt: processClaimDecision(...).
Warum wichtig: Wird in Use Case, Policies, Ports und Adapter zerlegt.
Long Parameter List
Erklärung: Anti-Pattern, bei dem eine Methode sehr viele Einzelparameter benötigt.
Beispiel im Lernprojekt: processClaimDecision mit ClaimId, User, Rollen, Dokumenten, Status, Beträgen.
Warum wichtig: Command Object bündelt Eingaben.
Primitive Obsession
Erklärung: Fachliche Konzepte werden nur als String, int oder BigDecimal modelliert.
Beispiel im Lernprojekt: Status als String, Betrag als BigDecimal ohne Bedeutung.
Warum wichtig: Value Objects machen Regeln explizit.
God Class
Erklärung: Klasse mit zu vielen Verantwortlichkeiten.
Beispiel im Lernprojekt: LegacyClaimFacadeBean.
Warum wichtig: Verantwortlichkeiten werden auf Module verteilt.
Hidden Side Effect
Erklärung: Nebenwirkung, die im Code nicht klar erkennbar ist.
Beispiel im Lernprojekt: DAO speichert, JMS sendet, Audit schreibt im gleichen Pfad.
Warum wichtig: Ports und Tests machen Seiteneffekte sichtbar.
Manual Error Handling
Erklärung: Fehlerbehandlung durch verstreute try/catch-Blöcke, Flags und technische Statuscodes.
Beispiel im Lernprojekt: SOAP-Timeout wird als fachliche Ablehnung interpretiert.
Warum wichtig: Result Objects und Policies trennen Technik von Fachlichkeit.
Technical Exception as Business Flow
Erklärung: Technische Exceptions steuern fachliche Entscheidungen.
Beispiel im Lernprojekt: Timeout bedeutet angeblich Fraud Reject.
Warum wichtig: Anti-Corruption Layer übersetzt sauber.
Test Data Builder
Erklärung: Testhilfe zum lesbaren Erzeugen komplexer Testobjekte.
Beispiel im Lernprojekt: ClaimBuilder, CommandBuilder.
Warum wichtig: Tests werden kürzer und aussagekräftiger.
Fake Object
Erklärung: Einfache Testimplementierung einer Abhängigkeit.
Beispiel im Lernprojekt: FakeFraudRiskPort.
Warum wichtig: Use Cases werden ohne SOAP/DB testbar.
Spy
Erklärung: Testobjekt, das Aufrufe aufzeichnet.
Beispiel im Lernprojekt: SpyAuditLogPort.
Warum wichtig: Belegt, dass Audit-Events erzeugt wurden.
Adapter Test
Erklärung: Test für eine technische Adapterimplementierung.
Beispiel im Lernprojekt: FraudRiskSoapAdapterTest.
Warum wichtig: Sichert Mapping, Fehlerfälle und Timeouts.
Contract Test
Erklärung: Test gegen ein vereinbartes Schnittstellenverhalten.
Beispiel im Lernprojekt: SOAP Contract Test für Partner.
Warum wichtig: Schützt externe Konsumenten.
Architecture Test
Erklärung: Automatisierter Test für Architekturregeln.
Beispiel im Lernprojekt: Domain darf Infrastructure nicht importieren.
Warum wichtig: Verhindert Rückfall in enge Kopplung.
ArchUnit
Erklärung: Java-Testbibliothek zur Prüfung von Paket- und Architekturregeln.
Beispiel im Lernprojekt: ArchitectureBoundaryTest.
Warum wichtig: Macht Architekturgrenzen dauerhaft prüfbar.
Behavior Preservation
Erklärung: Erhaltung des fachlich beobachtbaren Verhaltens während intern umgebaut wird.
Beispiel im Lernprojekt: Legacy-vs-Modular Vergleichstest.
Warum wichtig: Wichtig für risikoreiches Refactoring.
Incremental Refactoring
Erklärung: Umbau in kleinen, überprüfbaren Schritten statt Big Bang.
Beispiel im Lernprojekt: Phasen 1 bis 14.
Warum wichtig: Reduziert Migrationsrisiko.
Moderne Architektur und Domain Design
Hexagonale Architektur
Erklärung: Architekturstil mit Fachkern in der Mitte und Ports/Adapters an den Rändern.
Beispiel im Lernprojekt: claim-domain, claim-application, claim-infrastructure.
Warum wichtig: Domäne wird unabhängig von Technik.
Ports & Adapters
Erklärung: Muster, bei dem Fachlogik Ports definiert und technische Adapter diese Ports implementieren.
Beispiel im Lernprojekt: CoveragePort + CoverageSoapAdapter.
Warum wichtig: Integration austauschbar und testbar.
Port
Erklärung: Fachliche Schnittstelle, die beschreibt, was die Anwendung benötigt.
Beispiel im Lernprojekt: PaymentReleasePort.
Warum wichtig: Verhindert direkte SOAP-/JDBC-Abhängigkeit in Use Cases.
Adapter
Erklärung: Technische Implementierung eines Ports.
Beispiel im Lernprojekt: FileShareDocumentAdapter.
Warum wichtig: Kapselt alte Technik und Mapping.
Anti-Corruption Layer
Erklärung: Schutzschicht zwischen alter Integration und moderner Domäne.
Beispiel im Lernprojekt: FraudRiskSoapAdapter übersetzt XML in RiskScore.
Warum wichtig: Alte Begriffe verschmutzen die neue Domäne nicht.
Domain Model
Erklärung: Fachliches Modell mit Entitäten, Value Objects, Regeln und Policies.
Beispiel im Lernprojekt: Claim, ClaimStatus, RiskScore.
Warum wichtig: Fachlichkeit wird zentral und verständlich.
Entity
Erklärung: Objekt mit Identität und Lebenszyklus.
Beispiel im Lernprojekt: Claim.
Warum wichtig: Status und Entscheidungen werden konsistent verwaltet.
Value Object
Erklärung: Unveränderliches Objekt, das einen fachlichen Wert ausdrückt.
Beispiel im Lernprojekt: ClaimId, Money, RiskScore.
Warum wichtig: Validierung und Bedeutung liegen am richtigen Ort.
Aggregate
Erklärung: Konsistenzgrenze im Domain Model.
Beispiel im Lernprojekt: Claim mit Entscheidungen und Dokumentverweisen.
Warum wichtig: Schützt Invarianten bei Änderungen.
Repository Pattern
Erklärung: Abstraktion zum Laden und Speichern fachlicher Aggregate.
Beispiel im Lernprojekt: ClaimRepository.
Warum wichtig: Persistenzdetails bleiben außerhalb der Domäne.
Application Service
Erklärung: Use-Case-Schicht, die Transaktion, Ports und Domänenregeln koordiniert.
Beispiel im Lernprojekt: ProcessClaimDecisionHandler.
Warum wichtig: Ersetzt EJB-Monsterlogik.
Command Object
Erklärung: Objekt, das Eingaben eines Use Cases bündelt.
Beispiel im Lernprojekt: ProcessClaimDecisionCommand.
Warum wichtig: Ersetzt lange Parameterlisten.
Result Object
Erklärung: Objekt, das fachliches Ergebnis und Folgeinformationen strukturiert zurückgibt.
Beispiel im Lernprojekt: ProcessClaimDecisionResult.
Warum wichtig: Ersetzt technische Returncodes und Exceptions.
Policy Object
Erklärung: Kapselt eine fachliche Regel.
Beispiel im Lernprojekt: AuthorizationPolicy, NotificationTemplatePolicy.
Warum wichtig: Regeln sind einzeln testbar.
State Pattern
Erklärung: Statusabhängiges Verhalten wird aus if/else-Ketten in explizite Status-/Transitionslogik verschoben.
Beispiel im Lernprojekt: ClaimTransitionPolicy.
Warum wichtig: Statusübergänge werden verständlich.
Specification Pattern
Erklärung: Kombinierbare fachliche Prüfregeln.
Beispiel im Lernprojekt: CoverageSpecification.
Warum wichtig: Komplexe Regeln werden modular.
Facade Pattern
Erklärung: Vereinfachte Schnittstelle vor komplexem Subsystem.
Beispiel im Lernprojekt: LegacyClaimFacade als Übergangspunkt.
Warum wichtig: Ermöglicht erste sichere Schnittlinie.
Workflow Orchestrator
Erklärung: Komponente, die fachliche Prozessschritte koordiniert, ohne technische Details zu besitzen.
Beispiel im Lernprojekt: ClaimDecisionWorkflowOrchestrator.
Warum wichtig: Workflow wird nachvollziehbar und testbar.
Approval Chain
Erklärung: Genehmigungskette für Entscheidungen, Zahlungen oder Eskalationen.
Beispiel im Lernprojekt: Teamlead Approval vor Payment Release.
Warum wichtig: Ersetzt verstreute Rollenprüfungen.
Shared Kernel
Erklärung: Gemeinsamer Kern stabiler Typen, Fehler und Basisabstraktionen.
Beispiel im Lernprojekt: shared-kernel Modul.
Warum wichtig: Verhindert Duplikate und unklare Basistypen.
Bounded Context
Erklärung: Fachliche Grenze mit eigener Sprache und eigenen Modellen.
Beispiel im Lernprojekt: Claim, Payment, Document, Audit.
Warum wichtig: Vermeidet ein übergroßes gemeinsames Modell.
Zusatzsysteme und Integrationen
Partner Portal
Erklärung: Externes Portal für Partner wie Werkstätten, Makler oder Schadensmelder.
Beispiel im Lernprojekt: legacy-claims-partner-portal.
Warum wichtig: Kann über REST/BFF stranguliert werden.
Internal Backoffice
Erklärung: Interne Anwendung für Sachbearbeiter, Teamleiter und Spezialisten.
Beispiel im Lernprojekt: legacy-internal-claims-backoffice.
Warum wichtig: Backoffice API entkoppelt UI und Use Cases.
Document Management Adapter
Erklärung: Adapter zur Ablage, Prüfung, Versionierung und Archivierung von Dokumenten.
Beispiel im Lernprojekt: legacy-document-management-adapter.
Warum wichtig: File Share kann später ersetzt werden.
Document Storage Port
Erklärung: Schnittstelle zum Speichern und Laden von Dokumenten.
Beispiel im Lernprojekt: DocumentStoragePort.
Warum wichtig: Ablage ist austauschbar.
Document Verification Port
Erklärung: Schnittstelle zur fachlichen Dokumentenprüfung.
Beispiel im Lernprojekt: DocumentVerificationPort.
Warum wichtig: SOAP-Prüfdienst wird entkoppelt.
Fraud Risk Gateway
Erklärung: Integration zur Betrugsrisikoprüfung.
Beispiel im Lernprojekt: FraudRiskSoapClient.
Warum wichtig: RiskScore wird fachlich modelliert.
Fraud Risk Port
Erklärung: Fachlicher Port zur Risikoabfrage.
Beispiel im Lernprojekt: FraudRiskPort.
Warum wichtig: Timeouts und XML bleiben im Adapter.
Policy Coverage System
Erklärung: System zur Prüfung der Versicherungsdeckung.
Beispiel im Lernprojekt: PolicyCoverageSoapClient.
Warum wichtig: CoverageDecision ersetzt technische Antwortformate.
Coverage Port
Erklärung: Fachliche Schnittstelle zur Deckungsprüfung.
Beispiel im Lernprojekt: CoveragePort.
Warum wichtig: Regeln werden test- und austauschbar.
Payment Release System
Erklärung: Altsystem zur Freigabe und Auslösung von Zahlungen.
Beispiel im Lernprojekt: PaymentReleaseSoapClient.
Warum wichtig: PaymentReleasePort plus Idempotency schützt vor Doppelzahlung.
Payment Release Port
Erklärung: Schnittstelle zum Zahlungssystem.
Beispiel im Lernprojekt: PaymentReleasePort.
Warum wichtig: Kompensation und Fehlerbehandlung werden kontrolliert.
Notification System
Erklärung: System für E-Mail, SMS, interne Nachrichten und Templates.
Beispiel im Lernprojekt: LegacyNotificationSenderBean.
Warum wichtig: NotificationPort entkoppelt Benachrichtigung.
Notification Port
Erklärung: Fachlicher Port zum Versand von Benachrichtigungen.
Beispiel im Lernprojekt: NotificationPort.
Warum wichtig: Outbox und Retry werden möglich.
SLA & Escalation Scheduler
Erklärung: Scheduler für Fristen, Eskalationen und SLA-Verletzungen.
Beispiel im Lernprojekt: SlaEscalationTimerBean.
Warum wichtig: OpenShift CronJob und EscalationPolicy ersetzen TimerBean.
Audit & Compliance Reporter
Erklärung: System für Nachvollziehbarkeit, Reports und Compliance-Auswertungen.
Beispiel im Lernprojekt: AuditReportStoredProcedureDao.
Warum wichtig: AuditLogPort und Reporting Read Model trennen Verantwortung.
Search Indexer
Erklärung: Suchkomponente für Claims, Dokumente, Partnerfälle oder Kunden.
Beispiel im Lernprojekt: LegacyClaimSearchDao.
Warum wichtig: ClaimSearchPort trennt Suchdomäne.
Customer Master Data System
Erklärung: Stammdatensystem für Kundendaten, Adressen und Segmente.
Beispiel im Lernprojekt: CustomerMasterDataSoapClient.
Warum wichtig: CustomerSnapshot stabilisiert Daten.
Identity & Role Management
Erklärung: System für Benutzer, Gruppen, Rollen und Berechtigungen.
Beispiel im Lernprojekt: JaasRoleChecker.
Warum wichtig: SecurityContextPort + AuthorizationPolicy trennen Security vom Fachcode.
Messaging, Transaktionen und Resilienz
Outbox Pattern
Erklärung: Speichert fachliche Änderung und zu sendendes Event in derselben Datenbanktransaktion.
Beispiel im Lernprojekt: OutboxEvent nach Claim Decision.
Warum wichtig: Verhindert verlorene Events.
Idempotency
Erklärung: Operation kann mehrfach aufgerufen werden, ohne doppelte fachliche Wirkung.
Beispiel im Lernprojekt: Payment Idempotency Key.
Warum wichtig: Schützt bei Retry und Netzwerkfehlern.
Retry Policy
Erklärung: Regel für Wiederholungsversuche technischer Aufrufe.
Beispiel im Lernprojekt: Notification Retry.
Warum wichtig: Fehlerbehandlung wird explizit.
Circuit Breaker
Erklärung: Schaltet instabile externe Systeme vorübergehend ab.
Beispiel im Lernprojekt: FraudRiskService Timeout.
Warum wichtig: Verhindert Kaskadenausfälle.
Timeout Policy
Erklärung: Begrenzt Wartezeit auf externe Systeme.
Beispiel im Lernprojekt: SOAP-Aufruf zum Coverage System.
Warum wichtig: Threads und Transaktionen werden geschützt.
Dead Letter Queue
Erklärung: Ablage für Nachrichten, die nicht erfolgreich verarbeitet werden konnten.
Beispiel im Lernprojekt: Notification DLQ.
Warum wichtig: Fehler bleiben sichtbar und analysierbar.
Compensation
Erklärung: Ausgleichsaktion nach teilweisem Erfolg.
Beispiel im Lernprojekt: Payment stornieren, wenn Folgeprozess fehlschlägt.
Warum wichtig: Ersetzt unrealistische globale Transaktionen.
Exactly Once Illusion
Erklärung: Irrtum, dass verteilte Systeme immer exakt einmal ausführen können.
Beispiel im Lernprojekt: Payment und Notification.
Warum wichtig: Idempotency und Outbox sind realistischer.
At-Least-Once Delivery
Erklärung: Nachricht kann mindestens einmal, ggf. mehrfach ankommen.
Beispiel im Lernprojekt: Outbox Publisher.
Warum wichtig: Consumer müssen idempotent sein.
Transactional Boundary
Erklärung: Bewusste Grenze einer Transaktion.
Beispiel im Lernprojekt: Claim speichern + Outbox schreiben.
Warum wichtig: Externe SOAP-Aufrufe werden nicht blind in JTA gepackt.
Eventual Consistency
Erklärung: Systeme werden nach kurzer Zeit konsistent, nicht zwingend sofort.
Beispiel im Lernprojekt: Search Read Model nach Claim Update.
Warum wichtig: Erlaubt entkoppelte Verarbeitung.
Correlation ID
Erklärung: Kennung zur Verfolgung eines Vorgangs über Systemgrenzen.
Beispiel im Lernprojekt: Claim Decision Request.
Warum wichtig: Verbessert Debugging und Audit.
Lease Lock
Erklärung: Sperrmechanismus, damit Batchjobs in mehreren Pods nicht doppelt laufen.
Beispiel im Lernprojekt: EscalationBatchJob.
Warum wichtig: Kubernetes-Skalierung bleibt kontrolliert.
OpenShift, Container und Betrieb
OpenShift
Erklärung: Enterprise-Kubernetes-Plattform von Red Hat für Build, Deployment, Security und Betrieb.
Beispiel im Lernprojekt: Zielplattform.
Warum wichtig: Löst manuelle WebSphere-Deployments ab.
Containerfile
Erklärung: Anweisung zum Bauen eines Container-Images.
Beispiel im Lernprojekt: Containerfile.openliberty.
Warum wichtig: Runtime wird reproduzierbar.
Open Liberty
Erklärung: Leichtgewichtige Java-Enterprise-Runtime für containerisierte Workloads.
Beispiel im Lernprojekt: Open Liberty Zielruntime.
Warum wichtig: Näher an Jakarta/MicroProfile und Containerbetrieb.
Spring Boot
Erklärung: Java-Anwendungsframework mit eingebettetem Runtime-Modell.
Beispiel im Lernprojekt: Alternative Zielruntime.
Warum wichtig: Erlaubt moderne REST- und Betriebsendpunkte.
ConfigMap
Erklärung: Kubernetes/OpenShift-Objekt für nicht-geheime Konfiguration.
Beispiel im Lernprojekt: Feature Flags, URLs, Timeouts.
Warum wichtig: Ersetzt harte Konfiguration im Code.
Secret
Erklärung: Kubernetes/OpenShift-Objekt für sensible Konfiguration.
Beispiel im Lernprojekt: DB-Passwort, SOAP Credentials.
Warum wichtig: Keine Passwörter im Code.
Route
Erklärung: OpenShift-Ressource zur externen Erreichbarkeit eines Service.
Beispiel im Lernprojekt: claim-rest-api Route.
Warum wichtig: Kontrollierter Zugriff von außen.
Service
Erklärung: Kubernetes-Abstraktion für stabile Netzwerkadresse zu Pods.
Beispiel im Lernprojekt: claim-service.
Warum wichtig: Pods können ersetzt werden.
Deployment
Erklärung: Kubernetes-Ressource zur Verwaltung von Pods und Rollouts.
Beispiel im Lernprojekt: claim-service Deployment.
Warum wichtig: Reproduzierbare Releases.
CronJob
Erklärung: Kubernetes/OpenShift-Ressource für geplante Jobs.
Beispiel im Lernprojekt: Escalation CronJob.
Warum wichtig: Ersetzt EJB Timer.
Liveness Probe
Erklärung: Prüft, ob ein Container noch lebt.
Beispiel im Lernprojekt: Health Endpoint.
Warum wichtig: Defekte Pods werden neu gestartet.
Readiness Probe
Erklärung: Prüft, ob ein Pod bereit ist, Traffic anzunehmen.
Beispiel im Lernprojekt: DB- und Dependency Check.
Warum wichtig: Verhindert Traffic auf nicht bereite Pods.
Startup Probe
Erklärung: Prüft den Startvorgang langsamer Anwendungen.
Beispiel im Lernprojekt: Open Liberty Start.
Warum wichtig: Verhindert zu frühe Neustarts.
HPA
Erklärung: Horizontal Pod Autoscaler.
Beispiel im Lernprojekt: Skalierung nach CPU oder Metriken.
Warum wichtig: Betrieb reagiert auf Last.
PDB
Erklärung: PodDisruptionBudget.
Beispiel im Lernprojekt: Mindestverfügbarkeit bei Wartung.
Warum wichtig: Schützt gegen zu viele gleichzeitige Ausfälle.
NetworkPolicy
Erklärung: Kubernetes-Regel für Netzwerkzugriffe zwischen Pods.
Beispiel im Lernprojekt: Claim Service darf nur erlaubte Ziele erreichen.
Warum wichtig: Reduziert Angriffsfläche.
Observability
Erklärung: Sammelbegriff für Logs, Metriken, Traces und Health.
Beispiel im Lernprojekt: Structured Logs, Metrics, Tracing.
Warum wichtig: Betrieb wird nachvollziehbar.
Structured Logging
Erklärung: Logs mit Feldern statt nur Textzeilen.
Beispiel im Lernprojekt: claimId, correlationId, decision.
Warum wichtig: Bessere Suche und Analyse.
Metrics
Erklärung: Messwerte über Laufzeit und Verhalten.
Beispiel im Lernprojekt: Decision Duration, Error Count.
Warum wichtig: Monitoring und Alerting werden möglich.
Tracing
Erklärung: Verfolgung eines Requests über Service- und Systemgrenzen.
Beispiel im Lernprojekt: SOAP -> Handler -> Payment -> Outbox.
Warum wichtig: Hilft bei Fehleranalyse.
Dokumentation, Change Management und Migration
Before/After Mapping
Erklärung: Zuordnung von Legacy-Dateien zu modernen Modulen und Dateien.
Beispiel im Lernprojekt: LegacyClaimFacadeBean -> ProcessClaimDecisionHandler.
Warum wichtig: Migrationsnutzen wird nachvollziehbar.
Changelog V2
Erklärung: Strukturierte Änderungsdokumentation mit Kategorie, Grund, Risiko, Tests und Migrationsnutzen.
Beispiel im Lernprojekt: CHANGELOG_V2.md.
Warum wichtig: Architekturänderungen bleiben erklärbar.
Change Matrix
Erklärung: Tabellarische Übersicht über Änderungen und betroffene Module.
Beispiel im Lernprojekt: CHANGELOG_V2_MATRIX.csv.
Warum wichtig: Hilft bei Review und Audit.
Migration Decision
Erklärung: Dokumentierte Entscheidung mit Kontext, Option, Begründung und Risiko.
Beispiel im Lernprojekt: MIGRATION_DECISIONS.md.
Warum wichtig: Verhindert verlorenes Architekturwissen.
Risk Register
Erklärung: Liste technischer und fachlicher Risiken mit Gegenmaßnahmen.
Beispiel im Lernprojekt: RISKS.md.
Warum wichtig: Macht Migration planbarer.
Rollback Plan
Erklärung: Plan zur Rückkehr auf einen sicheren Zustand.
Beispiel im Lernprojekt: Cutover mit Rückfalloption.
Warum wichtig: Reduziert Produktionsrisiko.
Cutover
Erklärung: Geplanter Umschaltzeitpunkt von Legacy auf Zielsystem oder Teilfunktion.
Beispiel im Lernprojekt: SOAP Boundary parallel betreiben.
Warum wichtig: Migration wird kontrolliert.
Strangler Pattern
Erklärung: Neue Funktionalität wird schrittweise um das Legacy-System herum aufgebaut.
Beispiel im Lernprojekt: Partner Portal wird über REST/BFF entkoppelt.
Warum wichtig: Kein Big-Bang-Austausch nötig.
Parallel Run
Erklärung: Legacy und Zielsystem laufen zeitweise parallel zur Verifikation.
Beispiel im Lernprojekt: Legacy-vs-Modular Vergleich.
Warum wichtig: Erhöht Sicherheit vor Abschaltung.
Smoke Test
Erklärung: Schneller End-to-End-Test der wichtigsten Funktion nach Deployment.
Beispiel im Lernprojekt: REST Smoke IT.
Warum wichtig: Findet grobe Deploy-Fehler früh.
Migration Test
Erklärung: Test zur Absicherung der Funktion auf Zielplattform.
Beispiel im Lernprojekt: OpenShiftDeploymentSmokeIT.
Warum wichtig: Sichert Runtime- und Konfigurationswechsel.
Artifact Manifest
Erklärung: Maschinenlesbare Liste erzeugter Dateien und Pakete.
Beispiel im Lernprojekt: artifact_manifest.json.
Warum wichtig: Erleichtert Prüfung des Gesamtpakets.
Link Check
Erklärung: Prüfung interner Links und Anker.
Beispiel im Lernprojekt: link_check.md.
Warum wichtig: Verhindert kaputte Navigation.
PDF Check
Erklärung: Prüfung von PDF-Seiten, Inhaltsverzeichnis, Bookmarks und Lesbarkeit.
Beispiel im Lernprojekt: pdf_check.md.
Warum wichtig: Sichert brauchbare Ausgabe.
Fachbegriffe im Claims-Kontext
Claim
Erklärung: Schadensfall oder Leistungsfall, der gemeldet, geprüft, entschieden und ggf. bezahlt wird.
Beispiel im Lernprojekt: Claim Aggregate.
Warum wichtig: Zentrales fachliches Objekt.
Claim Status
Erklärung: Aktueller Zustand eines Schadensfalls.
Beispiel im Lernprojekt: CREATED, DOCUMENTS_PENDING, APPROVED.
Warum wichtig: Statuslogik wird über Policies gesteuert.
Claim Decision
Erklärung: Fachliche Entscheidung über den weiteren Verlauf eines Claims.
Beispiel im Lernprojekt: Approve, Reject, Escalate.
Warum wichtig: Use Case wird explizit modelliert.
Coverage
Erklärung: Deckungsprüfung, ob Police und Schaden zusammenpassen.
Beispiel im Lernprojekt: CoverageDecision.
Warum wichtig: Externe Tariflogik wird entkoppelt.
Deductible
Erklärung: Selbstbehalt, den der Kunde selbst tragen muss.
Beispiel im Lernprojekt: Coverage Calculation.
Warum wichtig: Fachwert statt bloßer Zahl.
Fraud Risk
Erklärung: Einschätzung eines möglichen Betrugsrisikos.
Beispiel im Lernprojekt: RiskScore.
Warum wichtig: Technische Risikoantwort wird fachlich nutzbar.
Document Verification
Erklärung: Prüfung von eingereichten Dokumenten auf Vollständigkeit und Gültigkeit.
Beispiel im Lernprojekt: verifyClaimDocument.
Warum wichtig: Eigene Port-Grenze statt Fachlogik im EJB.
Payment Approval
Erklärung: Fachliche Freigabe einer Auszahlung.
Beispiel im Lernprojekt: approveClaimPayment.
Warum wichtig: Approval Chain und Payment Port trennen Regeln und Technik.
Escalation
Erklärung: Hochstufung eines Falls bei Fristverletzung, Risiko oder Sonderentscheidung.
Beispiel im Lernprojekt: escalateClaim.
Warum wichtig: EscalationPolicy ersetzt TimerBean-Logik.
SLA
Erklärung: Service Level Agreement, also vereinbarte Bearbeitungsfrist oder Qualitätsvorgabe.
Beispiel im Lernprojekt: SLA & Escalation Scheduler.
Warum wichtig: Fristen werden testbar berechnet.
Audit Trail
Erklärung: Chronologische, fachlich verständliche Historie eines Falls.
Beispiel im Lernprojekt: getClaimAuditTrail.
Warum wichtig: Compliance wird nachvollziehbar.
Customer Snapshot
Erklärung: Stabile Momentaufnahme relevanter Kundendaten.
Beispiel im Lernprojekt: CustomerSnapshot Value Object.
Warum wichtig: Schützt vor inkonsistenten Stammdaten.
Partner Identity
Erklärung: Identität eines externen Partners mit Rollen und Zugriffen.
Beispiel im Lernprojekt: Partner Identity Service.
Warum wichtig: Partnerzugriffe werden kontrolliert.