Legacy Claims & Customer Support Enterprise System
Legacy Claims & Customer Support Enterprise System
Konsolidierte, ĂŒbersichtlich gegliederte Fassung aus dem Rohmaterial. Hauptkapitel sind bewusst wenige, groĂe Lerneinheiten. Unterpunkte wie 3.1 sind keine eigenen Kapitel mehr, sondern normale Abschnitte innerhalb des Hauptkapitels. Reine Planungsabschnitte wurden entfernt, damit das Lernbuch wie ein fertiges Handbuch wirkt.
1. EinfĂŒhrung und Lesehinweis
Kapitelziel
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.
Fachliche Visualisierungshinweise
Die HTML-Ausgabe nutzt kompakte fachliche SVG-Skizzen dort, wo sie das VerstÀndnis verbessern: Architekturgrenzen, Legacy-Kopplung, Refactoring-Fluss, Mapping, Risiken, Glossar und Codebeispiel-Leselogik. Diese Grafiken ersetzen keine Inhalte, sondern dienen als schnelle Orientierung vor detailreichen Abschnitten.
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
2. Zielbild und Gesamtstruktur
Kapitelziel
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
Fachlicher Mittelpunkt
Im Zentrum steht der Schadensfall:
Claim
ââ Kunde
ââ Police / Vertrag / Deckung
ââ Dokumente
ââ Fraud-Risiko
ââ Status / Workflow
ââ Entscheidung
ââ Zahlung
ââ Audit Trail
ââ SLA / Eskalation
ââ Kommunikation / Benachrichtigung
Der 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 Service
Zielmodule
Die Zielmodule bilden fachliche und technische Grenzen. Der Package-Stamm lautet immer:
com.seb4u.demo.claims
ModulĂŒ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.zip
Refactoring-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.json
Changelog-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.md
Mapping-Grundidee
Legacy-Datei / Legacy-Modul
-> neue Datei / neues Modul
-> Grund der Ănderung
-> Pattern / Architekturkonzept
-> Test / Sicherheitsnetz
-> Migrationsnutzen
Erste 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.security
ModulabhÀ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.java
Typische 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.md
Entscheidungen 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.
3. Legacy-Ausgangssystem verstehen
Kapitelziel
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.jar
Legacy-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.xml
Partner-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.xml
Internal-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.xml
WebSphere-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_ASSIGNMENT
Typische 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(...)
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
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
4. Refactoring Schritt fĂŒr Schritt
Kapitelziel
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
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.yml
cases:
- 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_REQUESTED
Legacy Characterization Test
Datei:
migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyProcessClaimDecisionCharacterizationTest.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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.java
package com.seb4u.demo.claims.shared.security;
public interface SecurityContextPort {
AuthenticatedActor currentActor();
}
Datei:
claim-domain/src/main/java/com/seb4u/demo/claims/domain/security/AuthorizationPolicy.java
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.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.java
package 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.java
package 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.java
package 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.java
package 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.java
package 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
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.java
package 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.java
package 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.java
package 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.java
package com.seb4u.demo.claims.search;
public interface ClaimSearchPort {
ClaimSearchResult search(ClaimSearchQuery query);
}
Datei:
claim-search/src/main/java/com/seb4u/demo/claims/search/ClaimSearchQuery.java
package 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
-> ProcessClaimDecisionHandler
SpÀter:
Modern Partner UI / BFF
-> claim-rest-api
-> ProcessClaimDecisionHandler
Datei:
legacy-claims-partner-portal/src/main/java/com/seb4u/demo/claims/portal/PartnerPortalClaimApplicationService.java
package 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.java
package 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.java
package 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.java
package 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.java
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 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.java
package 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.java
package 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.
5. Moderne Zielarchitektur und Maven-Struktur
Kapitelziel
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
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.xml
Beispiel:
<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.yaml
Beispiel: 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: string
Modul: 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.java
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-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 Tests
Domain 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.md
Diese 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
6. Migration nach OpenShift
Kapitelziel
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 Console
Nach 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 stack
Wichtig: 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
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.md
Containerisierung
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 1
ErklÀ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: prod
ServiceAccount
#### 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-service
ConfigMap 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: 9443
Route
#### 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: Redirect
SOAP- 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/ready
Health 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 Master
Migration 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 separat
OpenShift-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-pvc
Migrationsentscheidung
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.export
NetworkPolicy
#### 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: 443
ErklÀ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: 70
PodDisruptionBudget
#### 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-service
Observability
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_total
Tracing
Wichtige Trace-Spans:
ProcessClaimDecision
ââ LoadClaim
ââ AuthorizeActor
ââ VerifyDocument
ââ CheckCoverage
ââ CheckFraudRisk
ââ TransitionClaimState
ââ PreparePaymentRelease
ââ AppendAuditEvent
ââ AppendOutboxEvent
ââ PersistClaim
REST 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.
7. Lernbuch-Artefakte und Ausgabeformate
Kapitelziel
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 - 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());
}
}
8. Ănderungsdokumentation und Mapping
Kapitelziel
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());
}
}
9. Anhang A - Refactoring Phases
Kapitelziel
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
10. Anhang B - Essential Changes
Kapitelziel
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 |
11. Anhang C - Changelog V2
Kapitelziel
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
12. Anhang D - Before/After Mapping
Kapitelziel
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.
13. Anhang E - Migration Decisions
Kapitelziel
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.
14. Anhang F - Risks
Kapitelziel
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 |
15. Anhang G - Runde 07 Codebeispiele
Kapitelziel
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());
}
}
16. Anhang H - GroĂes Glossar
Kapitelziel
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.
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.
Warum wichtig: Verhindert verlorenes Architekturwissen.
Risk Register
ErklĂ€rung: Liste technischer und fachlicher Risiken mit GegenmaĂnahmen.
Beispiel im Lernprojekt: RISKS.
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.
Warum wichtig: Verhindert kaputte Navigation.
PDF Check
ErklĂ€rung: PrĂŒfung von PDF-Seiten, Inhaltsverzeichnis, Bookmarks und Lesbarkeit.
Beispiel im Lernprojekt: pdf_check.
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.