Legacy Claims Portal 🏠 Home 📘 Lernpfad 🧪 Workbook 📚 Handbuch 🧩 Patterns 🧾 Lizenzen 🔎 Archiv
VOLLSTÄNDIG · INTERAKTIV · v2

Legacy Claims & Customer Support Enterprise System

16klappbare Hauptkapitel
269Codebeispiele
34fachliche Tabellen
64SVG-Visualisierungen

Vollständige interaktive Lesefassung mit dem unveränderten Gesamtinhalt, zusätzlicher Schnellnavigation und fachlich kompakten Architekturvisualisierungen.

Konsolidierte, planungsbereinigte Fassung aus dem Rohmaterial. Reine Planungsabschnitte wurden entfernt.

Navigation

Schnellinhaltsverzeichnis

Alle Hauptkapitel sind vollständig enthalten. Ein Klick öffnet das Kapitel und springt direkt zum Inhalt.

011. Einführung und Lesehinweis
Kapitel 01 1. Einführung und Lesehinweis Kompakter Themen-Input als Orientierung zum Abschnitt

Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Einführung und Lesehinweis in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Projektüberblick

Konsolidierte, ausführliche Lernbuchfassung aus dem vorhandenen Rohmaterial. Diese Fassung strukturiert das Rohmaterial einheitlich, entfernt nur offensichtliche Chat-/Runden-Artefakte und bewahrt die fachlichen Inhalte, Codebeispiele, Tabellen, Mapping- und Migrationsabschnitte.

Lesehinweis

Dieses Handbuch ist als durchgehendes Lern- und Nachschlagewerk aufgebaut. Die HTML-Versionen enthalten klappbare Hauptkapitel, Navigation, Suche, echte Markdown-Tabellen, kompakte JetBrains-Dark-Codeblöcke und dezente SVG-Visuals.

Konsolidierte finale Fassung aus den Rohmaterial-Runden 01-07 und den Änderungsdokumenten. Der Inhalt wurde nicht gekürzt. Tabellen und Codeblöcke wurden für HTML/PDF strukturell sauber aufbereitet.

Konsolidierte Struktur

  • Teil 1: Zielbild und Systemlandschaft
  • Teil 2: Legacy-Ausgangssystem
  • Teil 3: Refactoring Schritt fuer Schritt
  • Teil 4: Moderne Maven-Zielstruktur
  • Teil 5: Migration nach OpenShift
  • Teil 6: Lernbuch-Artefakte
  • Teil 7: Refactoring-Phasen und Change-Dokumentation
  • Anhaenge: Changelog, Mapping, Risiken und Codebeispiele

022. Zielbild und Gesamtstruktur
Kapitel 02 2. Zielbild und Gesamtstruktur Kompakter Themen-Input als Orientierung zum Abschnitt

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:

  1. Legacy verstehen WebSphere Traditional, EAR/WAR/EJB, SOAP, JMS, JTA, Oracle, JSP/Servlet, JAAS/LDAP, File Shares, Scheduler, Stored Procedures und manuelle Deployments.

  2. Legacy absichern Characterization Tests, Golden Master Tests, SOAP Contract Tests, Adapter Tests, Migration Tests, Architekturtests und Testdaten-Builder.

  3. 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.

  4. 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.


Themenfokus Gesamt-Systemlandschaft

Gesamt-Systemlandschaft

Systemlandschaft als Verantwortungsfluss

Die fachliche Entscheidung bleibt im Claims Core; Kanäle und Partnersysteme werden über explizite Adapter angebunden.

Systemlandschaft Legacy ClaimsKanäle, Claims-Anwendung, Integrationsschicht und externe Systeme. Systemlandschaft: vom Eingang bis zu den Partnersystemen KANÄLE PartnerportalBackofficeSOAP / Batch CLAIMS CORE ValidierungEntscheidungStatus & SLA ADAPTER JMS / MQJDBC / JPAFiles / LDAP EXTERN CustomerPaymentDMS / Fraud
  • Kanäle liefern Anfragen, besitzen aber keine Claims-Regeln.
  • Adapter übersetzen Protokolle und Datenmodelle, ohne Fachlogik zu übernehmen.
  • Externe Systeme werden als eigene Ausfall- und Konsistenzgrenzen behandelt.
Verständnis-Skizze Systemlandschaft auf einen Blick
Kanäle, fachlicher Kern und Integrationen in einer kompakten Sicht.

Fachlicher Mittelpunkt

Im Zentrum steht der Schadensfall:

CODE
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

CODE
                               +----------------------------------+

                               | 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

CODE
+----------------------------------------------------------------------------------+

|                                  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| |
|  +------------------+ +---------------------+ +-------------------------------+ |
+----------------------------------------------------------------------------------+

Themenfokus Domänenlandkarte

Domänenlandkarte

Verständnis-Skizze Domänenlandkarte
Die wichtigsten fachlichen Bausteine und ihre Beziehungen.

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:

CODE
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:

CODE
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

CODE
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:

CODE
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
CODE
01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/
Vertiefung Refactoring-Phasen

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.

CODE
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:

CODE
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

Vertiefung Before/After-Mapping Zielstruktur

Before/After-Mapping Zielstruktur

Später werden erzeugt:

CODE
BEFORE_AFTER_MAPPING.md
BEFORE_AFTER_MAPPING.csv
BEFORE_AFTER_MAPPING.json
BEFORE_AFTER_MAPPING_KURZFASSUNG.md
Vertiefung Mapping-Grundidee

Mapping-Grundidee

CODE
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:

CODE
package com.seb4u.demo.claims...

Beispiele:

CODE
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

CODE
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

CODE
<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

CODE
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

CODE
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);
}
CODE
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);
}
CODE
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);
}
CODE
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);
}
CODE
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

CODE
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

CODE
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

CODE
               +-----------------------------+

               | 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

Themenfokus OpenShift-Zielbild

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

CODE
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.


033. Legacy-Ausgangssystem verstehen
Kapitel 03 3. Legacy-Ausgangssystem verstehen Kompakter Themen-Input als Orientierung zum Abschnitt

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

Verständnis-Skizze Legacy-Last sichtbar machen
Viele technische Abhängigkeiten laufen direkt durch den Monolithen.

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:

  1. Partner oder Sachbearbeiter erstellt Schaden.
  2. Dokumente werden hochgeladen.
  3. Kunde und Police werden geprüft.
  4. Betrugsrisiko wird bewertet.
  5. Dokumente werden technisch und fachlich verifiziert.
  6. Deckung und Selbstbehalt werden berechnet.
  7. Entscheidung wird vorbereitet.
  8. Teamleiter oder Spezialist genehmigt.
  9. Zahlung wird freigegeben.
  10. Kunde, Partner und interne Stellen werden benachrichtigt.
  11. Audit Trail, Reporting, Suche und SLA-Verarbeitung laufen parallel.

Kritischer Legacy-Kern

Der problematische Kern ist die Methode:

CODE
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.

CODE
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

CODE
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

CODE
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

CODE
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

CODE
<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

CODE
<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

CODE
<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

CODE
<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.

Themenfokus Legacy-Datenbankmodell und Stored Procedures

Legacy-Datenbankmodell und Stored Procedures

Wichtige Tabellen

CODE
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

CODE
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

CODE
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.

Themenfokus Legacy-SOAP-Operationen

Legacy-SOAP-Operationen

Interne SOAP-Fassade

Das alte System bietet eine SOAP-Fassade für Partner und andere Legacy-Systeme.

CODE
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.

Themenfokus JMS und IBM MQ im Legacy-System

JMS und IBM MQ im Legacy-System

JMS Publisher

CODE
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

CODE
<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>

Themenfokus Security: LDAP, JAAS und hart kodierte Rollen

Security: LDAP, JAAS und hart kodierte Rollen

JaasRoleChecker.java

CODE
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

CODE
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

CODE
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:

  • prepareCall wird 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

CODE
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

CODE
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

CODE
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

CODE
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 CoverageDecision Result Object.
  • Keine Specification oder CoveragePolicy.

PaymentReleaseSoapClient.java

CODE
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.

Themenfokus Die Monster Method: LegacyClaimFacadeBean.processClaimDecision(...)
Vertiefung Die Monster Method: LegacyClaimFacadeBean.processClaimDecision(...)

Die Monster Method: LegacyClaimFacadeBean.processClaimDecision(...)

Claims-Lebenszyklus hinter der Monster Method

Die lange Legacy-Methode vermischt mehrere fachliche Schritte. Der Lebenszyklus macht sichtbar, welche Entscheidungen später getrennt getestet und modularisiert werden.

Claims-LebenszyklusFünf fachliche Schritte von der Erfassung bis zur revisionssicheren Nachbearbeitung. Claims-Lebenszyklus 1. ErfassenClaim + Kunde 2. PrüfenPlausibilität 3. EntscheidenRegel + Rolle 4. AusführenZahlung / DMS 5. NachhaltenAudit + SLA Statuswechsel sind fachliche Entscheidungen - keine reinen Datenbankupdates.
  • Erfassung und Plausibilitätsprüfung sind von der Entscheidung zu trennen.
  • Zahlung, DMS und Benachrichtigung sind nachgelagerte Integrationen.
  • Audit und SLA begleiten den Prozess, ersetzen aber keine Domain-Regel.
Verständnis-Skizze Monster Method zerlegen
Ein Verfahren mit vielen Verantwortungen wird in saubere Schritte aufgeteilt.

Die folgende Methode ist absichtlich lang und schlecht. Sie ist das zentrale Refactoring-Objekt für die nächsten Runden.

LegacyClaimFacadeBean.java

CODE
package com.seb4u.demo.claims.legacy.ejb;

import com.seb4u.demo.claims.legacy.dao.LegacyClaimDao;
import com.seb4u.demo.claims.legacy.dao.LegacyClaimRecord;
import com.seb4u.demo.claims.legacy.dao.StoredProcedureDecisionResult;
import com.seb4u.demo.claims.legacy.document.LegacyDocumentShareClient;
import com.seb4u.demo.claims.legacy.jms.LegacyClaimEventPublisher;
import com.seb4u.demo.claims.legacy.security.JaasRoleChecker;
import com.seb4u.demo.claims.legacy.soap.*;

import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

@Stateless
public class LegacyClaimFacadeBean {

    @EJB
    private LegacyClaimDao claimDao;

    @EJB
    private LegacyClaimAuditBean auditBean;

    @EJB
    private LegacyClaimDocumentBean documentBean;

    @EJB
    private LegacyClaimPaymentBean paymentBean;

    @EJB
    private JaasRoleChecker roleChecker;

    @EJB
    private LegacyClaimEventPublisher eventPublisher;

    @EJB
    private LegacyClaimSearchBean searchBean;

    @EJB
    private LegacyNotificationSenderBean notificationSender;

    @Resource
    private SessionContext sessionContext;

    private FraudRiskSoapClient fraudRiskSoapClient = new FraudRiskSoapClient();
    private PolicyCoverageSoapClient policyCoverageSoapClient = new PolicyCoverageSoapClient();
    private CustomerMasterDataSoapClient customerMasterDataSoapClient = new CustomerMasterDataSoapClient();
    private DocumentVerificationSoapClient documentVerificationSoapClient = new DocumentVerificationSoapClient();
    private PaymentReleaseSoapClient paymentReleaseSoapClient = new PaymentReleaseSoapClient();
    private LegacyDocumentShareClient documentShareClient = new LegacyDocumentShareClient();

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public LegacyClaimDecisionResponse processClaimDecision(LegacyClaimDecisionRequest request) {
        long started = System.currentTimeMillis();
        List<String> technicalWarnings = new ArrayList<>();
        List<String> businessWarnings = new ArrayList<>();

        if (request == null) {
            throw new IllegalArgumentException("request must not be null");
        }
        if (request.getClaimId() == null) {
            throw new IllegalArgumentException("claimId must not be null");
        }
        if (request.getDecisionCode() == null || request.getDecisionCode().trim().isEmpty()) {
            throw new IllegalArgumentException("decisionCode must not be empty");
        }
        if (request.getActorUser() == null || request.getActorUser().trim().isEmpty()) {
            request.setActorUser(sessionContext.getCallerPrincipal().getName());
        }
        if (request.getCorrelationId() == null || request.getCorrelationId().trim().isEmpty()) {
            request.setCorrelationId("LEGACY-" + System.currentTimeMillis() + "-" + request.getClaimId());
        }

        auditBean.writeTechnicalAudit(
            request.getClaimId(),
            "PROCESS_DECISION_STARTED",
            request.getActorUser(),
            "Started decision " + request.getDecisionCode() + " with correlation " + request.getCorrelationId()
        );

        LegacyClaimRecord claim = claimDao.findByIdForUpdate(request.getClaimId());
        if (claim == null) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "PROCESS_DECISION_FAILED",
                request.getActorUser(),
                "Claim not found"
            );
            throw new IllegalStateException("Claim not found: " + request.getClaimId());
        }

        String oldStatus = claim.getStatus();
        String amountCategory;
        if (claim.getClaimAmount() == null) {
            amountCategory = "UNKNOWN";
        } else if (claim.getClaimAmount().compareTo(new BigDecimal("500")) <= 0) {
            amountCategory = "LOW";
        } else if (claim.getClaimAmount().compareTo(new BigDecimal("5000")) <= 0) {
            amountCategory = "MEDIUM";
        } else {
            amountCategory = "HIGH";
        }

        if (!roleChecker.canProcessDecision(oldStatus, request.getDecisionCode(), amountCategory)) {
            auditBean.writeSecurityAudit(
                request.getClaimId(),
                "DECISION_FORBIDDEN",
                request.getActorUser(),
                "roles=" + roleChecker.currentRoles() + ", status=" + oldStatus + ", decision=" + request.getDecisionCode()
            );
            throw new SecurityException("User is not allowed to process decision");
        }

        if ("CLOSED".equals(oldStatus) || "ARCHIVED".equals(oldStatus)) {
            auditBean.writeBusinessAudit(
                request.getClaimId(),
                "DECISION_REJECTED_INVALID_STATUS",
                request.getActorUser(),
                "Cannot process decision for status " + oldStatus
            );
            throw new IllegalStateException("Cannot process already closed or archived claim");
        }

        if ("APPROVE".equals(request.getDecisionCode()) && "NEW".equals(oldStatus)) {
            businessWarnings.add("Approving a NEW claim is unusual and may require manual review");
            if (!request.isManualOverride()) {
                throw new IllegalStateException("NEW claim cannot be approved without manual override");
            }
        }

        LegacyCustomerResponse customerResponse;
        try {
            customerResponse = customerMasterDataSoapClient.getCustomer(claim.getCustomerId());
            if (customerResponse == null || customerResponse.getCustomerId() == null) {
                auditBean.writeBusinessAudit(request.getClaimId(), "CUSTOMER_NOT_FOUND", request.getActorUser(), claim.getCustomerId());
                throw new IllegalStateException("Customer not found in master data");
            }
            if ("DECEASED".equals(customerResponse.getCustomerStatus())) {
                businessWarnings.add("Customer is marked as deceased in master data");
            }
            if (customerResponse.getAddressQuality() != null && customerResponse.getAddressQuality().compareTo(new BigDecimal("0.5")) < 0) {
                businessWarnings.add("Customer address quality is low");
            }
        } catch (Exception ex) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "CUSTOMER_SERVICE_ERROR",
                request.getActorUser(),
                ex.getClass().getName() + ": " + ex.getMessage()
            );
            throw new IllegalStateException("Customer master data service failed", ex);
        }

        LegacyCoverageResult coverageResult;
        try {
            coverageResult = policyCoverageSoapClient.calculateCoverage(
                claim.getPolicyNo(),
                request.getClaimType() == null ? "UNKNOWN" : request.getClaimType(),
                claim.getClaimAmount()
            );
            if (coverageResult == null) {
                throw new IllegalStateException("Coverage service returned null");
            }
            if (!coverageResult.isCovered()) {
                if ("APPROVE".equals(request.getDecisionCode())) {
                    auditBean.writeBusinessAudit(
                        request.getClaimId(),
                        "APPROVAL_BLOCKED_NO_COVERAGE",
                        request.getActorUser(),
                        "policyNo=" + claim.getPolicyNo()
                    );
                    throw new IllegalStateException("Cannot approve claim without policy coverage");
                }
                businessWarnings.add("Claim is not covered by policy");
            }
            if (coverageResult.getDeductibleAmount() != null && coverageResult.getDeductibleAmount().compareTo(BigDecimal.ZERO) > 0) {
                businessWarnings.add("Deductible amount applies: " + coverageResult.getDeductibleAmount());
            }
        } catch (RuntimeException ex) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "COVERAGE_SERVICE_ERROR",
                request.getActorUser(),
                ex.getMessage()
            );
            throw ex;
        } catch (Exception ex) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "COVERAGE_SERVICE_ERROR",
                request.getActorUser(),
                ex.getMessage()
            );
            throw new IllegalStateException("Coverage service failed", ex);
        }

        FraudRiskLegacyResponse fraudResponse;
        try {
            fraudResponse = fraudRiskSoapClient.checkRisk(
                claim.getClaimId(),
                claim.getCustomerId(),
                claim.getPolicyNo(),
                claim.getClaimAmount()
            );
            BigDecimal riskScore = new BigDecimal(fraudResponse.getScore());
            claimDao.updateRiskScore(claim.getClaimId(), riskScore);
            if (riskScore.compareTo(new BigDecimal("800")) >= 0) {
                if ("APPROVE".equals(request.getDecisionCode()) && !request.isManualOverride()) {
                    auditBean.writeBusinessAudit(
                        request.getClaimId(),
                        "APPROVAL_BLOCKED_HIGH_RISK",
                        request.getActorUser(),
                        "riskScore=" + riskScore
                    );
                    throw new IllegalStateException("High fraud risk requires manual override");
                }
                businessWarnings.add("High fraud risk score: " + riskScore);
            }
            if (fraudResponse.isManualReviewRequired() && !request.isManualOverride()) {
                auditBean.writeBusinessAudit(
                    request.getClaimId(),
                    "MANUAL_REVIEW_REQUIRED",
                    request.getActorUser(),
                    fraudResponse.getRiskCategory()
                );
                if ("APPROVE".equals(request.getDecisionCode())) {
                    throw new IllegalStateException("Manual fraud review required before approval");
                }
            }
        } catch (Exception ex) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "FRAUD_SERVICE_ERROR",
                request.getActorUser(),
                ex.getClass().getName() + ": " + ex.getMessage()
            );
            if ("APPROVE".equals(request.getDecisionCode())) {
                throw new IllegalStateException("Cannot approve because fraud service failed", ex);
            }
            technicalWarnings.add("Fraud service failed: " + ex.getMessage());
            fraudResponse = new FraudRiskLegacyResponse();
            fraudResponse.setScore("999");
            fraudResponse.setRiskCategory("UNKNOWN_TECHNICAL_ERROR");
            fraudResponse.setManualReviewRequired(true);
        }

        if ("APPROVE".equals(request.getDecisionCode()) || "REJECT".equals(request.getDecisionCode())) {
            List<LegacyDocumentMetadata> documents = documentBean.findDocumentsForClaim(request.getClaimId());
            if (documents == null || documents.isEmpty()) {
                if ("APPROVE".equals(request.getDecisionCode())) {
                    auditBean.writeBusinessAudit(
                        request.getClaimId(),
                        "APPROVAL_BLOCKED_NO_DOCUMENTS",
                        request.getActorUser(),
                        "No documents found"
                    );
                    throw new IllegalStateException("Cannot approve without documents");
                }
                businessWarnings.add("No documents found");
            } else {
                boolean allDocumentsReadable = true;
                boolean allDocumentsVerified = true;
                for (LegacyDocumentMetadata doc : documents) {
                    if (doc.getAbsolutePath() == null || !documentShareClient.exists(doc.getAbsolutePath())) {
                        allDocumentsReadable = false;
                        auditBean.writeTechnicalAudit(
                            request.getClaimId(),
                            "DOCUMENT_FILE_MISSING",
                            request.getActorUser(),
                            "documentId=" + doc.getDocumentId() + ", path=" + doc.getAbsolutePath()
                        );
                    } else {
                        try {
                            LegacyDocumentVerificationResult result = documentVerificationSoapClient.verify(
                                doc.getDocumentId(),
                                doc.getAbsolutePath(),
                                doc.getDocumentType()
                            );
                            if (result == null || !result.isVerified()) {
                                allDocumentsVerified = false;
                                auditBean.writeBusinessAudit(
                                    request.getClaimId(),
                                    "DOCUMENT_NOT_VERIFIED",
                                    request.getActorUser(),
                                    "documentId=" + doc.getDocumentId()
                                );
                            }
                        } catch (Exception ex) {
                            technicalWarnings.add("Document verification failed for " + doc.getDocumentId());
                            allDocumentsVerified = false;
                            auditBean.writeTechnicalAudit(
                                request.getClaimId(),
                                "DOCUMENT_VERIFICATION_ERROR",
                                request.getActorUser(),
                                ex.getMessage()
                            );
                        }
                    }
                }

                if (!allDocumentsReadable && "APPROVE".equals(request.getDecisionCode())) {
                    throw new IllegalStateException("Cannot approve because some document files are missing");
                }
                if (!allDocumentsVerified && "APPROVE".equals(request.getDecisionCode()) && !request.isManualOverride()) {
                    throw new IllegalStateException("Cannot approve because some documents are not verified");
                }
                claimDao.markDocumentStatus(request.getClaimId(), allDocumentsVerified ? "VERIFIED" : "MANUAL_REVIEW");
            }
        }

        String effectiveDecision = request.getDecisionCode();
        if ("APPROVE".equals(effectiveDecision)) {
            if (coverageResult.getApprovedAmount() != null && coverageResult.getApprovedAmount().compareTo(BigDecimal.ZERO) <= 0) {
                auditBean.writeBusinessAudit(
                    request.getClaimId(),
                    "APPROVAL_BLOCKED_ZERO_AMOUNT",
                    request.getActorUser(),
                    "approvedAmount=" + coverageResult.getApprovedAmount()
                );
                throw new IllegalStateException("Approved amount must be positive");
            }
            if ("HIGH".equals(amountCategory) && !roleChecker.currentRoles().contains("CLAIMS_MANAGER")) {
                throw new SecurityException("High amount approval requires CLAIMS_MANAGER");
            }
        } else if ("REJECT".equals(effectiveDecision)) {
            if (request.getReason() == null || request.getReason().length() < 20) {
                throw new IllegalArgumentException("Reject reason must contain at least 20 characters");
            }
        } else if ("ESCALATE".equals(effectiveDecision)) {
            if (request.getReason() == null || request.getReason().trim().isEmpty()) {
                request.setReason("Escalated from backoffice without detailed reason");
            }
        } else if ("REQUEST_MORE_INFO".equals(effectiveDecision)) {
            if (request.getPartnerMessage() == null || request.getPartnerMessage().trim().isEmpty()) {
                throw new IllegalArgumentException("Partner message required for more info request");
            }
        } else {
            throw new IllegalArgumentException("Unknown decision code: " + effectiveDecision);
        }

        StoredProcedureDecisionResult storedProcedureResult = claimDao.callProcessClaimDecision(
            request.getClaimId(),
            effectiveDecision,
            request.getActorUser(),
            request.getReason()
        );
        String newStatus = storedProcedureResult.getNewStatus();

        auditBean.writeBusinessAudit(
            request.getClaimId(),
            "DECISION_PROCESSED",
            request.getActorUser(),
            "oldStatus=" + oldStatus + ", newStatus=" + newStatus + ", decision=" + effectiveDecision
        );

        if ("APPROVE".equals(effectiveDecision)) {
            try {
                BigDecimal approvedAmount = coverageResult.getApprovedAmount() == null
                    ? claim.getClaimAmount()
                    : coverageResult.getApprovedAmount();

                if (coverageResult.getDeductibleAmount() != null) {
                    approvedAmount = approvedAmount.subtract(coverageResult.getDeductibleAmount());
                }
                if (approvedAmount.compareTo(BigDecimal.ZERO) < 0) {
                    approvedAmount = BigDecimal.ZERO;
                }

                if (request.isForcePayment() || approvedAmount.compareTo(BigDecimal.ZERO) > 0) {
                    LegacyPaymentReleaseResponse paymentResponse = paymentReleaseSoapClient.releasePayment(
                        request.getClaimId(),
                        claim.getCustomerId(),
                        approvedAmount,
                        request.getActorUser()
                    );
                    if (paymentResponse == null || !"OK".equals(paymentResponse.getStatus())) {
                        auditBean.writeTechnicalAudit(
                            request.getClaimId(),
                            "PAYMENT_RELEASE_FAILED",
                            request.getActorUser(),
                            paymentResponse == null ? "null response" : paymentResponse.getErrorMessage()
                        );
                        throw new IllegalStateException("Payment release failed");
                    }
                    paymentBean.markPaymentReleased(request.getClaimId(), paymentResponse.getPaymentReference(), approvedAmount);
                }
            } catch (Exception ex) {
                auditBean.writeTechnicalAudit(
                    request.getClaimId(),
                    "PAYMENT_ERROR",
                    request.getActorUser(),
                    ex.getMessage()
                );
                throw new IllegalStateException("Claim approved but payment failed", ex);
            }
        }

        try {
            eventPublisher.publishDecisionChanged(
                request.getClaimId(),
                oldStatus,
                newStatus,
                request.getActorUser(),
                request.getCorrelationId()
            );
        } catch (Exception ex) {
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "EVENT_PUBLISH_FAILED",
                request.getActorUser(),
                ex.getMessage()
            );
            throw ex;
        }

        try {
            if ("APPROVE".equals(effectiveDecision)) {
                notificationSender.sendClaimApprovedNotification(request.getClaimId(), claim.getCustomerId(), claim.getPartnerId());
            } else if ("REJECT".equals(effectiveDecision)) {
                notificationSender.sendClaimRejectedNotification(request.getClaimId(), claim.getCustomerId(), claim.getPartnerId(), request.getReason());
            } else if ("ESCALATE".equals(effectiveDecision)) {
                notificationSender.sendEscalationNotification(request.getClaimId(), request.getActorUser(), request.getReason());
            } else if ("REQUEST_MORE_INFO".equals(effectiveDecision)) {
                notificationSender.sendMoreInfoRequest(request.getClaimId(), claim.getPartnerId(), request.getPartnerMessage());
            }
        } catch (Exception ex) {
            technicalWarnings.add("Notification failed: " + ex.getMessage());
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "NOTIFICATION_FAILED",
                request.getActorUser(),
                ex.getMessage()
            );
        }

        try {
            searchBean.refreshClaimSearchCache(request.getClaimId());
        } catch (Exception ex) {
            technicalWarnings.add("Search refresh failed: " + ex.getMessage());
            auditBean.writeTechnicalAudit(
                request.getClaimId(),
                "SEARCH_REFRESH_FAILED",
                request.getActorUser(),
                ex.getMessage()
            );
        }

        if ("ESCALATE".equals(effectiveDecision)) {
            try {
                claimDao.createEscalationRecord(
                    request.getClaimId(),
                    request.getActorUser(),
                    request.getReason(),
                    Instant.now().plusSeconds(86400).toString()
                );
            } catch (Exception ex) {
                auditBean.writeTechnicalAudit(
                    request.getClaimId(),
                    "ESCALATION_RECORD_FAILED",
                    request.getActorUser(),
                    ex.getMessage()
                );
                throw new IllegalStateException("Could not create escalation record", ex);
            }
        }

        long duration = System.currentTimeMillis() - started;
        auditBean.writeTechnicalAudit(
            request.getClaimId(),
            "PROCESS_DECISION_FINISHED",
            request.getActorUser(),
            "durationMs=" + duration + ", technicalWarnings=" + technicalWarnings + ", businessWarnings=" + businessWarnings
        );

        LegacyClaimDecisionResponse response = new LegacyClaimDecisionResponse();
        response.setClaimId(request.getClaimId());
        response.setOldStatus(oldStatus);
        response.setNewStatus(newStatus);
        response.setDecisionCode(effectiveDecision);
        response.setAuditId(storedProcedureResult.getAuditId());
        response.setCorrelationId(request.getCorrelationId());
        response.setTechnicalWarnings(technicalWarnings);
        response.setBusinessWarnings(businessWarnings);
        response.setMessage("Decision processed with status " + newStatus);
        return response;
    }
}

Warum diese Methode schlecht ist

Diese Methode verletzt fast jedes wichtige Refactoring-Prinzip:

Problem Beispiel Folge
Zu viele Verantwortlichkeiten Security, Dokumente, Payment, Audit, JMS, Search schwer testbar
Fachlogik in EJB Statusregeln direkt in Bean WebSphere-Abhängigkeit
Externe Systeme direkt SOAP-Clients im Ablauf keine Ports/Adapter
Transaktion zu groß JTA über DB, SOAP, JMS Seiteneffekte schwer kontrollierbar
Fehlerlogik chaotisch IllegalStateException für Fachfälle keine klare Fehlersemantik
Statuslogik verteilt Java + Stored Procedure schwer nachvollziehbar
Rollen hart kodiert CLAIMS_MANAGER im Code Security-Migration schwer
Dokumente hart gekoppelt File Share direkt Object Storage schwer
Payment synchron Freigabe im Entscheidungsablauf keine Idempotenz
Event ohne Outbox JMS direkt Inkonsistenz möglich
Tests schwierig EJB/JNDI/SOAP/JMS nötig kaum Unit Tests möglich

Erste Refactoring-Kandidaten aus der Monster Method

Bereich Späterer Zielbaustein Pattern / Konzept
Request-Validierung ProcessClaimDecisionCommand Command Pattern
Rollenprüfung AuthorizationPolicy Policy Object
Statuswechsel ClaimState, ClaimTransitionPolicy State Pattern
Coverage CoveragePort Port & Adapter
Fraud FraudRiskPort Anti-Corruption Layer
Dokumentprüfung DocumentVerificationPort Port & Adapter
Payment PaymentReleasePort Outbox, Idempotency
Audit AuditLogPort Audit Boundary
Notification NotificationPort Outbox / Adapter
Suche ClaimSearchPort Read Model / Adapter
SLA/Eskalation EscalationPolicy Batch Worker
Orchestrierung ClaimDecisionWorkflowOrchestrator Workflow Orchestrator

Legacy SLA & Escalation Scheduler

Customer Support und SLA-Eskalation

Support-Triage, fachliche Bearbeitung und SLA-Eskalation benötigen klare Übergaben und nachvollziehbare Verantwortungen.

Customer-Support und EskalationSLA-gesteuerter Weg vom Eingang über Triage und Fachteams bis zur Lösung. Customer Support: Triage, SLA und Eskalation EINGANGPortal / Telefon TRIAGEPriorität + Owner FACHTEAMClaim / Payment / DMS ESKALATIONSLA-Risiko / Ausnahme LÖSUNGKunde + Audit SLA-Timer erzeugt Arbeit - nicht automatisch eine fachliche Entscheidung.
  • Die Triage klassifiziert und weist zu.
  • Fachteams lösen Claims-, Zahlungs- oder Dokumentprobleme.
  • Eine Eskalation erzeugt Sichtbarkeit und Priorität, aber keine automatische Genehmigung.

SlaEscalationTimerBean.java

CODE
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

CODE
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 status und partnerId.
  • 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

CODE
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

CODE
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.

Vertiefung Legacy-Dateien für späteres Before/After-Mapping

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

Vertiefung Risiken, die vor Refactoring verstanden werden müssen

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.xml
  • ibm-application-bnd.xml
  • ejb-jar.xml
  • persistence.xml
  • SP_PROCESS_CLAIM_DECISION
  • SP_REFRESH_CLAIM_REPORTING
  • LegacyClaimSoapEndpoint.java
  • LegacyClaimEventPublisher.java
  • JaasRoleChecker.java
  • LegacyDocumentShareClient.java
  • LegacyClaimDao.java
  • LegacyPartnerPortalServlet.java
  • InternalClaimsBackofficeBean.java
  • FraudRiskSoapClient.java
  • PolicyCoverageSoapClient.java
  • PaymentReleaseSoapClient.java
  • LegacyClaimFacadeBean.processClaimDecision(...)
  • SlaEscalationTimerBean.java
  • LegacyClaimSearchDao.java
  • AuditReportStoredProcedureDao.java
  • LegacyNotificationSenderBean.java

044. Refactoring Schritt für Schritt
Kapitel 04 4. Refactoring Schritt für Schritt Kompakter Themen-Input als Orientierung zum Abschnitt

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:

CODE
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:

  1. Verhalten sichtbar machen.
  2. Sicherheitsnetz mit Tests aufbauen.
  3. Refactoring-Seams schaffen.
  4. Fachliche Begriffe herausarbeiten.
  5. technische Abhängigkeiten hinter Ports verstecken.
  6. Status- und Workflowlogik aus der EJB herausziehen.
  7. Integrationen über Adapter kapseln.
  8. Transaktionen und Events kontrolliert neu schneiden.
  9. erst danach Richtung OpenShift, REST, BFF, moderne UI und modulare Maven-Struktur gehen.

Merksatz:

Migration ohne Refactoring verschiebt nur Chaos in Container. Refactoring ohne Sicherheitsnetz erzeugt neues Chaos. Deshalb: erst verstehen, dann testen, dann schneiden, dann migrieren.


Refactoring-Roadmap dieser Runde

Schrittweises Strangler-Refactoring

Die Modernisierung bleibt kontrollierbar, wenn jeder Schnitt durch Tests abgesichert und technisch reversibel ist.

Sicheres RefactoringSchrittweise Ablösung mit Charakterisierungstests, Seams, Extraktion und Ports. Sicheres Refactoring statt Big Bang 12345 Verhalten sichernSeam setzenLogik extrahierenPorts einführenAlt abschalten Golden MasterFassade / AdapterDomain / PolicyInbound / OutboundMessbar & reversibel
  • Zuerst Verhalten erfassen, danach Strukturen verändern.
  • Seams und Ports reduzieren Kopplung, ohne sofort Systeme abzuschalten.
  • Alte Pfade werden erst nach messbarer Gleichwertigkeit entfernt.
Verständnis-Skizze Refactoring-Roadmap
Vom Sicherheitsnetz über fachliche Schnitte bis zur entkoppelten Architektur.

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:

CODE
migration-tests/src/test/resources/golden-master/claim-decision-cases.yml
CODE
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:

CODE
migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyProcessClaimDecisionCharacterizationTest.java
CODE
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.

Themenfokus Golden Master Snapshot
Vertiefung Golden Master Snapshot

Golden Master Snapshot

Datei:

CODE
migration-tests/src/test/java/com/seb4u/demo/claims/migration/GoldenMasterSnapshot.java
CODE
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:

CODE
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:

CODE
before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimFacadeBean.java
CODE
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:

CODE
before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyDecisionInput.java
CODE
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:

CODE
after_refactoring_modular/claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionCommand.java
CODE
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:

CODE
claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionResult.java
CODE
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:

  • claimId
  • decision
  • status
  • actorUserId
  • policyNumber

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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/ClaimId.java
CODE
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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/RequestedDecision.java
CODE
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:

CODE
claim-domain/src/test/java/com/seb4u/demo/claims/domain/model/RequestedDecisionTest.java
CODE
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:

CODE
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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicy.java
CODE
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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionContext.java
CODE
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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/TransitionDecision.java
CODE
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:

CODE
claim-domain/src/test/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicyTest.java
CODE
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

Verständnis-Skizze Ports & Adapter greifbar machen
Der Use Case kennt nur Ports, technische Details liegen außen.

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:

CODE
claim-document/src/main/java/com/seb4u/demo/claims/document/DocumentVerificationPort.java
CODE
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:

CODE
claim-document/src/main/java/com/seb4u/demo/claims/document/FileShareDocumentVerificationAdapter.java
CODE
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

CODE
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:

CODE
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/FraudRiskPort.java
CODE
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:

CODE
claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/fraud/FraudRiskSoapAdapter.java
CODE
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:

CODE
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CoveragePort.java
CODE
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:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/coverage/CoveragePolicy.java
CODE
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

CODE
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:

CODE
claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentReleasePort.java
CODE
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:

CODE
claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentPreparationRequest.java
CODE
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:

CODE
claim-payment/src/main/java/com/seb4u/demo/claims/payment/IdempotencyKey.java
CODE
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:

CODE
claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CustomerMasterDataPort.java
CODE
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:

CODE
claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/customer/CustomerMasterDataSoapAdapter.java
CODE
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:

CODE
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:

CODE
shared-kernel/src/main/java/com/seb4u/demo/claims/shared/security/SecurityContextPort.java
CODE
package com.seb4u.demo.claims.shared.security;

public interface SecurityContextPort {

    AuthenticatedActor currentActor();
}

Datei:

CODE
claim-domain/src/main/java/com/seb4u/demo/claims/domain/security/AuthorizationPolicy.java
CODE
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

CODE
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

Vertiefung Schritt 13: ProcessClaimDecisionHandler als Application Service

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:

CODE
claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandler.java
CODE
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:

CODE
claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowOrchestrator.java
CODE
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:

CODE
claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowStep.java
CODE
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:

CODE
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.


Schritt 15: Audit Trail abstrahieren

Datei:

CODE
claim-audit/src/main/java/com/seb4u/demo/claims/audit/AuditLogPort.java
CODE
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:

CODE
claim-audit/src/main/java/com/seb4u/demo/claims/audit/StructuredAuditLogAdapter.java
CODE
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

Verlässlicher Ereignisfluss mit Outbox

Die Outbox koppelt das fachliche Ergebnis atomar an ein veröffentlichbares Ereignis und entkoppelt die technische Zustellung.

Outbox und Event-IntegrationTransaktionale Speicherung, Outbox Publisher, Broker und idempotente Konsumenten. Verlässliche Integration mit Outbox und idempotenten Konsumenten USE CASEClaim entscheideneine Transaktion DATABASEClaimOutbox Eventatomar gespeichert PUBLISHERRetry + Markierungat-least-once CONSUMERAudit / Searchidempotent Fachtransaktion und Event-Erzeugung bleiben gekoppelt; Zustellung wird technisch entkoppelt.
  • Claim und Outbox-Eintrag entstehen in derselben Datenbanktransaktion.
  • Der Publisher darf wiederholen; Konsumenten müssen idempotent sein.
  • Monitoring unterscheidet fachliche Fehler von Zustellproblemen.

Was war vorher schlecht?

JMS-Nachrichten wurden direkt aus der EJB gesendet. Dadurch entstehen klassische Fehler:

  • DB-Status gespeichert, Event nicht gesendet.
  • Event gesendet, DB-Rollback passiert.
  • Retry erzeugt doppelte Nachrichten.

Was wird geändert?

Events werden zuerst in eine Outbox-Tabelle geschrieben. Ein Worker publiziert sie später.

Datei:

CODE
claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxEvent.java
CODE
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:

CODE
claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxPublisherWorker.java
CODE
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:

CODE
claim-notification/src/main/java/com/seb4u/demo/claims/notification/NotificationPort.java
CODE
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:

CODE
claim-search/src/main/java/com/seb4u/demo/claims/search/ClaimSearchPort.java
CODE
package com.seb4u.demo.claims.search;

public interface ClaimSearchPort {

    ClaimSearchResult search(ClaimSearchQuery query);
}

Datei:

CODE
claim-search/src/main/java/com/seb4u/demo/claims/search/ClaimSearchQuery.java
CODE
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

CODE
LegacyPartnerPortalServlet
  -> InitialContext.lookup("ejb/LegacyClaimFacadeBean")
  -> processClaimDecision(...)

Nachher schrittweise

CODE
LegacyPartnerPortalServlet
  -> PartnerPortalClaimApplicationService
  -> ProcessClaimDecisionHandler

Später:

CODE
Modern Partner UI / BFF
  -> claim-rest-api
  -> ProcessClaimDecisionHandler

Datei:

CODE
legacy-claims-partner-portal/src/main/java/com/seb4u/demo/claims/portal/PartnerPortalClaimApplicationService.java
CODE
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:

CODE
legacy-internal-claims-backoffice/src/main/java/com/seb4u/demo/claims/backoffice/ClaimBackofficeUseCase.java
CODE
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:

CODE
claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationPolicy.java
CODE
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:

CODE
claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationBatchJob.java
CODE
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:

CODE
migration-tests/src/test/java/com/seb4u/demo/claims/architecture/ArchitectureBoundaryTest.java
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 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:

CODE
migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyVsModularDecisionComparisonTest.java
CODE
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_STATUS muss 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

Vertiefung Teil M: Before/After-Mapping-Vorbereitung aus Runde 3

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:

  1. Legacy-Verhalten einfrieren.
  2. Fachlogik sichtbar machen.
  3. technische Abhängigkeiten kapseln.
  4. fachliche Module schneiden.
  5. Tests aufbauen.
  6. 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:

  1. Fachliche Value Objects
  2. Transition Policy
  3. Authorization Policy
  4. Ports für externe Systeme
  5. Command Handler
  6. Audit Port
  7. Outbox
  8. Workflow Orchestrator
  9. Batch Worker
  10. REST API
  11. 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:

CODE
claim-application/src/test/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandlerTest.java
CODE
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
  • AuthorizationPolicy
  • ProcessClaimDecisionHandler
  • 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:

  1. Ziel der Refactoring-Runde
  2. Refactoring-Grundregel
  3. Roadmap der Refactoring-Schritte
  4. Inventory und Characterization
  5. Golden Master Snapshots
  6. Refactoring Seam
  7. Command Object
  8. Result Object
  9. Value Objects
  10. ClaimTransitionPolicy
  11. DocumentVerificationPort
  12. FraudRiskPort
  13. CoveragePort und CoveragePolicy
  14. PaymentReleasePort und Idempotency
  15. CustomerMasterDataPort
  16. SecurityContextPort und AuthorizationPolicy
  17. ProcessClaimDecisionHandler
  18. Workflow Orchestrator
  19. AuditLogPort
  20. Outbox Pattern
  21. NotificationPort
  22. ClaimSearchPort
  23. Partner Portal Entkopplung
  24. Backoffice Entkopplung
  25. EscalationPolicy und BatchJob
  26. ArchUnit Architekturtests
  27. Legacy-vs-Modular Vergleichstest
  28. Refactoring-Matrix
  29. Before/After-Mapping-Vorbereitung
  30. Risiken und offene Punkte

Logische Module:

  • claim-domain
  • claim-application
  • claim-document
  • claim-payment
  • claim-audit
  • claim-notification
  • claim-search
  • claim-workflow
  • claim-batch
  • claim-infrastructure
  • migration-tests
  • legacy-claims-partner-portal
  • legacy-internal-claims-backoffice

Wichtige Codebeispiele dieser Runde

  • LegacyProcessClaimDecisionCharacterizationTest.java
  • GoldenMasterSnapshot.java
  • LegacyDecisionInput.java
  • ProcessClaimDecisionCommand.java
  • ProcessClaimDecisionResult.java
  • ClaimId.java
  • RequestedDecision.java
  • ClaimTransitionPolicy.java
  • ClaimTransitionContext.java
  • TransitionDecision.java
  • DocumentVerificationPort.java
  • FileShareDocumentVerificationAdapter.java
  • FraudRiskPort.java
  • FraudRiskSoapAdapter.java
  • CoveragePort.java
  • CoveragePolicy.java
  • PaymentReleasePort.java
  • IdempotencyKey.java
  • CustomerMasterDataPort.java
  • SecurityContextPort.java
  • AuthorizationPolicy.java
  • ProcessClaimDecisionHandler.java
  • ClaimDecisionWorkflowOrchestrator.java
  • AuthorizationWorkflowStep.java
  • AuditLogPort.java
  • StructuredAuditLogAdapter.java
  • OutboxEvent.java
  • OutboxPublisherWorker.java
  • NotificationPort.java
  • ClaimSearchPort.java
  • PartnerPortalClaimApplicationService.java
  • ClaimBackofficeUseCase.java
  • EscalationPolicy.java
  • EscalationBatchJob.java
  • ArchitectureBoundaryTest.java
  • LegacyVsModularDecisionComparisonTest.java
  • ProcessClaimDecisionHandlerTest.java

Runde-3-Merksätze

  • Refactoring beginnt nicht mit schönem Code, sondern mit sicherem Verhalten.
  • Eine Monster Method wird nicht durch eine neue Monster-Klasse ersetzt.
  • Ports sind nicht nur Interfaces, sondern Migrationsgrenzen.
  • Policies machen Fachregeln sichtbar.
  • Outbox trennt Fachentscheidung von technischer Zustellung.
  • Idempotency schützt vor gefährlichen Wiederholungen.
  • Golden Master Tests schützen beim Schneiden, ersetzen aber nicht fachliche Tests.
  • OpenShift löst keine fachliche Kopplung. Refactoring muss vorher passieren.

Ende Runde 03.


055. Moderne Zielarchitektur und Maven-Struktur
Kapitel 05 5. Moderne Zielarchitektur und Maven-Struktur Kompakter Themen-Input als Orientierung zum Abschnitt

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:

  1. Fachlogik unabhängig von WebSphere machen.
  2. EJB-, SOAP-, JMS-, JDBC-, FileShare- und LDAP-Abhängigkeiten an den Rand verschieben.
  3. Fachliche Use Cases sichtbar machen.
  4. Workflow-Regeln testbar machen.
  5. Legacy-SOAP-Kompatibilität erhalten.
  6. Moderne REST/OpenAPI-Schicht ergänzen.
  7. Module für Dokumente, Suche, Audit, Zahlung, Notification, Batch und Security trennen.
  8. Migration nach OpenShift vorbereiten.
  9. Tests als Sicherheitsnetz neben die Architektur stellen.
  10. 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

Verständnis-Skizze Moderne Modulstruktur
Ein schlanker Kern mit klaren Schnittstellen und separaten Adaptern.

Empfohlene Root-Struktur:

CODE
legacy-claims-customer-support-enterprise/
  pom.xml
  README.md
  docs/
    architecture/
    migration/
    refactoring/
    testing/
    operations/
  before_refactoring_legacy/
  refactoring_steps/
  after_refactoring_modular/
  migration_to_openshift/
  tests/
  modules/
    shared-kernel/
    claim-domain/
    claim-application/
    claim-workflow/
    claim-document/
    claim-search/
    claim-notification/
    claim-audit/
    claim-payment/
    claim-infrastructure/
    claim-soap-api/
    claim-rest-api/
    claim-openapi/
    claim-batch/
    migration-tests/
    legacy-websphere-ear/
    legacy-claims-partner-portal/
    legacy-internal-claims-backoffice/
    legacy-document-management-adapter/
    legacy-fraud-risk-gateway/
    legacy-policy-coverage-system/
    legacy-payment-release-system/
    legacy-notification-system/
    legacy-sla-escalation-scheduler/
    legacy-audit-compliance-reporter/
    legacy-search-indexer/
    legacy-customer-master-data-system/
    legacy-identity-role-management/

Diese Struktur ist didaktisch absichtlich groß. In einem echten Projekt würde man manche Module später zusammenlegen oder als separate Repositories schneiden. Für das Lernprojekt ist die Sichtbarkeit der Grenzen wichtiger als minimale Dateianzahl.


Architekturprinzipien der Zielstruktur

Modulare Architektur mit fachlichem Zentrum

Die Zielarchitektur schützt Domain und Use Cases vor Protokollen, Datenbanken und Plattformdetails.

Modulare ZielarchitekturDomain im Zentrum, Application Services, Ports und technische Adapter. Modulare Zielarchitektur: fachlicher Kern, klare Grenzen CLAIM DOMAINAggregate · Policy · Value Objects INBOUNDREST · SOAP · Batch APPLICATIONUse Cases · Handler OUTBOUNDDB · MQ · DMS PLATFORMSecurity · Observability
  • Domain-Objekte kennen keine REST-, SOAP- oder OpenShift-APIs.
  • Application Services orchestrieren Use Cases über Ports.
  • Adapter bleiben austauschbar und werden separat getestet.

Domain bleibt frei von Infrastruktur

Das Modul claim-domain darf keine Abhängigkeit haben auf:

  • Spring
  • Jakarta EE
  • JPA
  • SOAP
  • JMS
  • JDBC
  • OpenShift
  • WebSphere
  • Servlet API
  • REST Controller
  • Oracle Driver

Erlaubt sind:

  • reine Java-Klassen
  • Value Objects
  • Domain Services
  • Policies
  • fachliche Exceptions
  • fachliche Events

Application orchestriert Use Cases

Das Modul claim-application kennt:

  • Commands
  • Handlers
  • Use Cases
  • Ports
  • Transaktionsgrenzen als abstraktes Konzept
  • Idempotency
  • Outbox als fachliche Integrationsabsicht

Es kennt nicht direkt:

  • SOAP-Client-Implementierungen
  • JMS-Templates
  • File-System-Pfade
  • JNDI-Namen
  • konkrete Oracle Stored Procedures

Infrastructure implementiert technische Details

Das Modul claim-infrastructure implementiert Ports:

  • SOAP Adapter
  • JMS Adapter
  • JDBC/JPA Adapter
  • FileShare Adapter
  • LDAP Adapter
  • Audit Adapter
  • Search Adapter
  • Payment Adapter
  • Notification Adapter

API-Module bleiben dünn

claim-soap-api und claim-rest-api sollen keine Fachlogik enthalten.

Sie tun nur:

  • Request entgegennehmen
  • DTO validieren
  • DTO in Command mappen
  • Use Case aufrufen
  • Result in Response mappen
  • technische Fehler in API-Fehler übersetzen

Legacy-Module bleiben als Lern- und Migrationsanker erhalten

Die Legacy-Module werden nicht gelöscht. Sie bleiben im Projekt sichtbar, damit Before/After-Mapping, Refactoring-Phasen und Migration verständlich bleiben.


Moderne Maven Parent POM

Datei:

CODE
pom.xml

Beispiel:

CODE
<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

CODE
<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

CODE
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

CODE
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

CODE
<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

CODE
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

CODE
package com.seb4u.demo.claims.domain;

public enum ClaimDecisionType {
    APPROVE,
    REJECT,
    REQUEST_MORE_DOCUMENTS,
    ESCALATE,
    SEND_TO_MANUAL_REVIEW,
    RELEASE_PAYMENT
}

Code: ClaimTransitionPolicy

CODE
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

CODE
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

CODE
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:

  • ProcessClaimDecisionCommand
  • ProcessClaimDecisionHandler
  • ClaimRepository
  • FraudRiskPort
  • CoveragePort
  • DocumentVerificationPort
  • PaymentReleasePort
  • AuditLogPort
  • OutboxPort
  • SecurityContextPort
  • AuthorizationPolicy

POM

CODE
<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

CODE
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

CODE
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

CODE
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);
}
CODE
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);
}
CODE
package com.seb4u.demo.claims.application.port;

import java.math.BigDecimal;

public interface CoveragePort {
    CoverageResult checkCoverage(String policyNumber, BigDecimal requestedAmount);
}
CODE
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);
}
CODE
package com.seb4u.demo.claims.application.port;

public interface CustomerMasterDataPort {
    CustomerSnapshot loadCustomerSnapshot(String customerNumber);
}
CODE
package com.seb4u.demo.claims.application.port;

public interface SecurityContextPort {
    UserSecurityContext loadUserContext(String actorUserId);
}

Code: ProcessClaimDecisionHandler

CODE
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

CODE
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:

  • DocumentStoragePort
  • DocumentVerificationPort
  • DocumentArchivePort
  • FileShareDocumentAdapter
  • ArchiveDocumentAdapter
  • später S3/Object Storage möglich

Code: DocumentStoragePort

CODE
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

CODE
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:

  • PaymentReleasePort
  • PaymentReleaseSoapAdapter
  • Idempotency Key
  • Outbox Event
  • Compensation-Strategie

Code: PaymentReleaseAdapter

CODE
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

CODE
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

CODE
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);
    }
}

Zweck

Suche wird aus Oracle-LIKE-Queries herausgelöst.

Legacy:

CODE
SELECT * FROM CLAIMS
WHERE UPPER(CUSTOMER_NAME) LIKE UPPER('%'||?||'%')
   OR UPPER(CLAIM_NR) LIKE UPPER('%'||?||'%')

Modern:

  • ClaimSearchPort
  • ClaimSearchCriteria
  • ClaimSearchResult
  • SearchReadModel
  • später OpenSearch/Elasticsearch möglich

Code: ClaimSearchPort

CODE
package com.seb4u.demo.claims.search;

public interface ClaimSearchPort {
    ClaimSearchPage search(ClaimSearchCriteria criteria);
}

Code: OracleClaimSearchAdapter

CODE
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

CODE
package com.seb4u.demo.claims.notification;

public interface NotificationPort {
    void send(NotificationMessage message);
}

Code: NotificationTemplatePolicy

CODE
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

CODE
<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

CODE
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

CODE
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

CODE
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:

CODE
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

CODE
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:

  • @Schedule oder 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

CODE
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:

CODE
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

CODE
<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

CODE
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:

CODE
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("jdbc/ClaimsDS");
String path = System.getProperty("claims.document.share");

Modern:

CODE
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

CODE
package com.seb4u.demo.claims.application.port;

public interface SecurityContextPort {
    UserSecurityContext loadUserContext(String userId);
}

Code: AuthorizationPolicy

CODE
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

CODE
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:

  1. Claim wird in Oracle geändert.
  2. JMS Event wird gesendet.
  3. Payment SOAP wird aufgerufen.
  4. Notification wird publiziert.
  5. Irgendwo tritt ein Fehler auf.
  6. Der fachliche Zustand und die Integration sind nicht mehr konsistent.

Modern:

  1. Claim wird gespeichert.
  2. Outbox Event wird in derselben DB-Transaktion gespeichert.
  3. Worker publiziert Event später zuverlässig.
  4. Retry und Dead Letter sind möglich.

Code: JdbcOutboxPort

CODE
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

CODE
Migration Tests
    ^
API Contract Tests
    ^
Adapter Tests
    ^
Application Use Case Tests
    ^
Domain Tests

Domain Test

CODE
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

CODE
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

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:

CODE
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.


Vertiefung Before/After-Mapping aus Runde 4

Before/After-Mapping aus Runde 4

Legacy-Datei Moderne Datei Grund Pattern
LegacyClaimFacadeBean.java ProcessClaimDecisionHandler.java Monster Method zerlegt Command Handler
LegacyClaimStatusIfElse.java ClaimTransitionPolicy.java Statuslogik getrennt Policy Object / State
LegacyDocumentShareClient.java DocumentStoragePort + FileShareDocumentAdapter FileShare entkoppelt Port/Adapter
FraudRiskSoapClient.java FraudRiskPort + FraudRiskSoapAdapter SOAP entkoppelt Anti-Corruption Layer
PolicyCoverageSoapClient.java CoveragePort + CoverageSoapAdapter Coverage getrennt Port/Adapter
PaymentReleaseSoapClient.java PaymentReleasePort + PaymentReleaseSoapAdapter Zahlung entkoppelt Idempotency / Adapter
LegacyNotificationSenderBean.java NotificationPort + NotificationAdapter Notification getrennt Outbox / Port
SlaEscalationTimerBean.java EscalationBatchJob + EscalationPolicy TimerBean getrennt Batch Worker / Policy
AuditReportStoredProcedureDao.java AuditLogPort + StructuredAuditLogAdapter Audit fachlich gemacht Audit Boundary
LegacyClaimSearchDao.java ClaimSearchPort + SearchIndexAdapter Suche getrennt Read Model / Adapter
CustomerMasterDataSoapClient.java CustomerMasterDataPort + CustomerMasterDataSoapAdapter Kundendaten entkoppelt ACL
JaasRoleChecker.java SecurityContextPort + AuthorizationPolicy Rollenlogik getrennt Security Boundary

Changelog-V2-Vorbereitung aus Runde 4

Diese Runde liefert Stoff für folgende Changelog-V2-Einträge:

ID Änderung Kategorie
CHG-004-001 Maven Parent und Module eingeführt Build / Struktur
CHG-004-002 Domain-Modul frameworkfrei gemacht Architektur
CHG-004-003 Application-Modul mit Use Cases und Ports eingeführt Architektur
CHG-004-004 Infrastructure-Modul für Adapter eingeführt Integration
CHG-004-005 SOAP Boundary kompatibel gehalten Migration
CHG-004-006 REST/OpenAPI ergänzt Modernisierung
CHG-004-007 Claim Workflow sichtbar gemacht Facharchitektur
CHG-004-008 Document Adapter getrennt Integration
CHG-004-009 Payment mit Idempotency vorbereitet Integration
CHG-004-010 Audit Trail strukturiert Compliance
CHG-004-011 Search als Port modelliert Performance / Entkopplung
CHG-004-012 SecurityContextPort und AuthorizationPolicy eingeführt Security
CHG-004-013 Batch/SLA als Worker vorbereitet Betrieb
CHG-004-014 Architekturtests ergänzt Qualität

Risiken der Zielstruktur

Zu viele Module

Risiko:

Für kleine Teams kann eine zu feine Modulstruktur schwer zu pflegen sein.

Gegenmaßnahme:

  • im Lernprojekt sichtbar lassen
  • in echtem Projekt später schneiden
  • Modulgrenzen nach Änderungsfrequenz und Teamstruktur prüfen

Falscher Shared Kernel

Risiko:

shared-kernel wird zum Müllplatz für alles.

Gegenmaßnahme:

  • strenge Regeln
  • Code Reviews
  • ArchUnit Tests

Ports ohne fachlichen Sinn

Risiko:

Man erzeugt Ports nur, weil es „hexagonal“ klingt.

Gegenmaßnahme:

  • Ports nur dort, wo Austauschbarkeit, Testbarkeit oder Runtime-Grenze relevant ist

REST zu früh als Ersatz für SOAP

Risiko:

Partner-Systeme brechen.

Gegenmaßnahme:

  • SOAP-Kompatibilität erhalten
  • REST parallel aufbauen
  • Contract Tests schreiben

Infrastructure schleicht zurück in Application

Risiko:

Application-Code importiert wieder SOAP, JDBC oder JMS.

Gegenmaßnahme:

  • Architekturtests
  • Modulabhängigkeiten in Maven streng halten

Ergebnis der Runde 4

Diese Runde hat das moderne Zielprojekt mit modularer Maven-Struktur definiert.

Erzeugt wurden:

  • Root-Struktur
  • Parent-POM
  • Modul-POMs
  • Domain-Modell
  • Application Use Case
  • Ports
  • Workflow-Modul
  • Document-Modul
  • Payment-Modul
  • Audit-Modul
  • Search-Modul
  • Notification-Modul
  • REST API
  • SOAP-kompatible Boundary
  • OpenAPI-Beispiel
  • Batch-Struktur
  • Infrastructure-Struktur
  • Security Boundary
  • Outbox-Struktur
  • Tests
  • Architekturtests
  • Before/After-Mapping-Vorbereitung
  • Changelog-V2-Vorbereitung

066. Migration nach OpenShift
Kapitel 06 6. Migration nach OpenShift Kompakter Themen-Input als Orientierung zum Abschnitt

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:

  1. Welche Legacy-Runtime-Abhängigkeiten entfernt oder ersetzt werden müssen.
  2. Welche Module containerfähig sind.
  3. Welche Konfigurationen nicht mehr in WebSphere liegen dürfen.
  4. Wie JNDI, JAAS, EJB Timer, JMS, File Shares, SOAP, Oracle und Batchjobs in OpenShift abgebildet werden.
  5. Wie man SOAP-kompatibel bleibt und trotzdem REST/OpenAPI ergänzt.
  6. Wie Deployment, Health Checks, Observability, Security und Betrieb aufgebaut werden.
  7. Welche Migrationstests nötig sind, damit die Umstellung nicht blind erfolgt.

Migration: Leitidee

Von WebSphere Traditional zu OpenShift

Verständnis-Skizze OpenShift-Zielbild
Von der alten Runtime in containerisierte, klar konfigurierte Bausteine.

Vorher war das System stark an WebSphere Traditional gebunden:

CODE
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:

CODE
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:

  1. Legacy stabilisieren Characterization Tests, Golden Master Tests, SOAP Contract Tests.

  2. Runtime-Seams schaffen Fachlogik darf nicht mehr direkt von WebSphere APIs abhängig sein.

  3. Konfiguration externalisieren Keine harten JNDI-Namen, keine festen File-Pfade, keine lokalen Properties im EAR.

  4. Containerfähige Zielruntime bauen Open Liberty oder Spring Boot/Jakarta Hybrid.

  5. SOAP-kompatible Fassade beibehalten Partner und Altsysteme werden nicht sofort gebrochen.

  6. REST/OpenAPI ergänzen Neue Clients nutzen moderne API, alte Clients nutzen SOAP weiter.

  7. Batch/Scheduler nach OpenShift CronJob verschieben EJB Timer wird nicht 1:1 übernommen.

  8. JMS/Outbox sauber machen Events werden nicht mehr innerhalb der Monster Method blind publiziert.

  9. Security modernisieren JAAS/WebSphere-Rollen werden in SecurityContextPort und AuthorizationPolicy überführt.

  10. Betriebsfähigkeit herstellen Health, Metrics, Logs, Tracing, Rollback, Ressourcenlimits, Probes.


Ziel-Runtime-Optionen

Option A: Open Liberty als Jakarta-Runtime

Diese Option ist für WebSphere-Migrationen oft didaktisch sauber, weil viele Java-EE/Jakarta-Konzepte wiedererkennbar bleiben.

Vorteile:

  • JAX-RS, CDI, JPA, Bean Validation, JAX-WS teilweise näher an Legacy-Welt
  • gute Brücke von Java EE zu Jakarta
  • geeignet für SOAP-Kompatibilität
  • leichter zu erklären für EAR/EJB-Migration

Nachteile:

  • trotzdem müssen EJB-Spezifika reduziert werden
  • nicht alle alten WebSphere-Features sind 1:1 portabel
  • manche Teams bevorzugen Spring Boot

Option B: Spring Boot als moderne Runtime

Vorteile:

  • breites Ökosystem
  • starke OpenShift- und Cloud-Native-Unterstützung
  • Actuator für Health/Metrics
  • gute REST/OpenAPI-Unterstützung

Nachteile:

  • größerer mentaler Sprung von EJB/JAX-WS
  • SOAP-Kompatibilität muss bewusst gebaut werden
  • JTA/JMS/JPA-Verhalten muss gut verstanden werden

Empfehlung für dieses Lernprojekt

Für das Lernprojekt wird ein Hybrid-Zielbild dokumentiert:

  • Open Liberty als Hauptvariante für WebSphere-nahe Migration.
  • Spring Boot als alternative Runtime-Variante für moderne Teams.
  • Domain/Application/Ports bleiben unabhängig von beiden.

Das ist wichtig, weil Architektur nicht an das Framework gekettet werden soll.


Ziel-Deployment-Struktur

OpenShift-Deployment als kontrollierter Prozess

Der Zielbetrieb trennt Build, Konfiguration, Deployment, Verifikation und Rollout, damit Änderungen nachvollziehbar und rückrollbar bleiben.

OpenShift-ZielbetriebBuild, Konfiguration, Deployment, Health-Probes, Routing und kontrollierter Rollout. OpenShift-Zielbetrieb: reproduzierbar, beobachtbar, rückrollbar BUILDImage + SBOM CONFIGSecret + Map DEPLOYPod + ServiceRoute VERIFYProbe + Metric ROLLOUTCanary / Rollback Datenmigration, externe Adapter und Scheduler werden separat abgesichert und aktiviert.
  • Images bleiben unveränderlich; Konfiguration und Secrets liegen außerhalb.
  • Readiness und Liveness prüfen unterschiedliche Zustände.
  • Rollout und Rollback werden mit Metriken und Smoke Tests abgesichert.
CODE
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

CODE
#### 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

CODE
<!-- 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

CODE
#### 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

CODE
#### 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

CODE
#### 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

CODE
#### 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

CODE
#### 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

CODE
#### 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: 2 erzwingt, dass die Anwendung nicht mehr implizit Single-Node-Logik haben darf.
  • maxUnavailable: 0 reduziert Downtime beim Rolling Update.
  • readinessProbe verhindert Traffic auf nicht bereite Pods.
  • livenessProbe erkennt hängende Prozesse.
  • startupProbe schützt langsame Starts vor falschen Restarts.
  • emptyDir ersetzt keine Dokumentenablage, sondern nur temporäre Runtime-Dateien.
  • Dokumente dürfen nicht mehr hart in lokalen Pfaden gespeichert werden.

Service und Route

Service

CODE
#### 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

CODE
#### 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:

CODE
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

CODE
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

CODE
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

CODE
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:

CODE
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

CODE
-- 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);
CODE
-- 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

CODE
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

CODE
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
    ) {
    }
}
CODE
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

CODE
#### 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

CODE
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

CODE
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

CODE
#### 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

CODE
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
);
CODE
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);
}
CODE
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:

CODE
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

CODE
DocumentStoragePort
 ├─ FileShareDocumentAdapter       Übergangsadapter
 ├─ ArchiveDocumentAdapter         Legacy Archive Service
 ├─ ObjectStorageDocumentAdapter   später S3/NooBaa/MinIO
 └─ DocumentVerificationPort       fachliche Prüfung separat

OpenShift-Konfiguration für Übergangs-FileShare

CODE
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claims-document-share-pvc
  namespace: claims-prod
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  storageClassName: managed-nfs-storage
CODE
### 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

CODE
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

CODE
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);
        }
    }
}
CODE
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

CODE
#### 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

CODE
#### 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

CODE
#### 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

CODE
#### 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

Security, Audit und Observability zusammendenken

Identität, Autorisierung und technische Telemetrie bilden zusammen den belastbaren Nachweis einer Claims-Entscheidung.

Security und ObservabilityIdentität, Autorisierung, Audit sowie Logs, Metriken und Traces als zusammenhängende Betriebsgrenze. Security und Observability als durchgängige Betriebsgrenze IDENTITÄTLDAP / OIDCRolle + Mandantwer handelt? POLICYAutorisierungfachliche Regelndarf es passieren? NACHWEISAudit EventLog · Metric · Tracewas ist geschehen?
  • Die Identität liefert Rolle, Mandant und Bearbeiterkontext.
  • Policies entscheiden fachlich und technisch über Berechtigungen.
  • Audit Events, Logs, Metriken und Traces beantworten unterschiedliche Nachweisfragen.

Logging

Ziel:

  • JSON Logs,
  • Correlation ID,
  • Claim ID nur kontrolliert,
  • keine sensiblen Kundendaten im Log,
  • klare technische und fachliche Events.
CODE
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:

CODE
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:

CODE
ProcessClaimDecision
 ├─ LoadClaim
 ├─ AuthorizeActor
 ├─ VerifyDocument
 ├─ CheckCoverage
 ├─ CheckFraudRisk
 ├─ TransitionClaimState
 ├─ PreparePaymentRelease
 ├─ AppendAuditEvent
 ├─ AppendOutboxEvent
 └─ PersistClaim

REST Smoke Test

CODE
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

CODE
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

CODE
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

CODE
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

CODE
### 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

Verständnis-Skizze Cutover kompakt
Parallelbetrieb, Umschalten und Rollback als kontrollierte Sequenz.

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

CODE
#### 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.

Vertiefung Migration Decisions

Migration Decisions

CODE
#### 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

CODE
#### RISKS.md

### R-001: Unvollständige SOAP-Kompatibilität

Auswirkung:
Partner oder Legacy-Systeme können nicht migrieren.

Gegenmaßnahme:
SOAP Contract Tests, WSDL-Vergleich, Pilotpartner.

### R-002: Doppelte Zahlungen

Auswirkung:
Finanzieller Schaden.

Gegenmaßnahme:
Payment Idempotency Key, PaymentReleasePort, Vergleichstest, Outbox.

### R-003: Doppelte Eskalationen

Auswirkung:
Falsche Benachrichtigungen, Prozesschaos.

Gegenmaßnahme:
CronJob concurrencyPolicy Forbid, Batch Lease Lock, idempotente Statusänderung.

### R-004: FileShare nicht containerfähig

Auswirkung:
Dokumente nicht erreichbar.

Gegenmaßnahme:
DocumentStoragePort, PVC/RWX Übergang, Object-Storage-Zielbild.

### R-005: Security-Rollen falsch gemappt

Auswirkung:
Unberechtigte Bearbeitung oder blockierte Nutzer.

Gegenmaßnahme:
RoleMapping Tests, Pilotgruppen, Audit Events.

### R-006: Stored Procedure Seiteneffekte vergessen

Auswirkung:
Fachliche Abweichungen.

Gegenmaßnahme:
Golden Master Tests, DB-Diff, Reporting-Vergleich.

Before/After-Migrationsmapping Runde 5

Legacy OpenShift-Ziel Grund Sicherheitsnetz
WebSphere EAR claim-service Deployment Runtime entkoppeln Deployment Smoke Test
EJB Timer OpenShift CronJob Scheduler portabel machen Batch Lease Lock Test
direkte JMS-Publizierung Outbox + Publisher CronJob Transaktionssicherheit OutboxPublisherIT
JNDI DataSource Config/Secret/DataSource Config reproduzierbare Konfiguration OracleConnectivityIT
JAAS SessionContext SecurityContextPort Security abstrahieren RoleMappingIT
FileShare Hardcoding DocumentStoragePort + PVC/Object Storage Dokumentenablage austauschbar DocumentAdapterIT
WebSphere Console Config YAML/ConfigMap/Secret GitOps-fähig Structure Check
technische Logs JSON Logs + Audit Events Observability Log Sampling Check
manuelles Deployment OpenShift Manifests/Pipeline Wiederholbarkeit Pipeline Smoke Test

Changelog-V2-Vorbereitung für Runde 5

Neue Changelog-Kandidaten:

ID Kategorie Änderung
CHG-OS-001 Runtime WebSphere EAR in OpenShift Deployment überführt
CHG-OS-002 Container Open Liberty Containerfile ergänzt
CHG-OS-003 Container Spring Boot Alternative ergänzt
CHG-OS-004 Config WebSphere Console Config durch ConfigMap/Secret ersetzt
CHG-OS-005 Health Liveness/Readiness/Startup Probes eingeführt
CHG-OS-006 Batch EJB Timer durch OpenShift CronJob ersetzt
CHG-OS-007 Messaging direkte JMS-Publizierung durch Outbox Publisher ersetzt
CHG-OS-008 Security JAAS-Rollenmapping in AuthorizationPolicy überführt
CHG-OS-009 Storage FileShare-Strategie containerfähig gemacht
CHG-OS-010 Observability Logging/Metrics/Tracing ergänzt
CHG-OS-011 Network NetworkPolicy für externe Systeme vorbereitet
CHG-OS-012 Tests OpenShift Migration Tests ergänzt
CHG-OS-013 Rollback Cutover- und Rollback-Plan ergänzt

Zusammenfassung Runde 5

Diese Runde hat die Migration von der WebSphere-Traditional-Landschaft nach OpenShift konkretisiert:

  • Containerfiles für Open Liberty und Spring Boot,
  • OpenShift Namespace, ServiceAccount, ConfigMap, Secret,
  • Deployment, Service, Route,
  • Health Checks,
  • Externalized Configuration,
  • Oracle-Migrationsstrategie,
  • JMS/Outbox-Migration,
  • SLA/Escalation CronJob,
  • Batch Lease Lock,
  • Document Storage Migration,
  • Security Migration,
  • NetworkPolicy,
  • HPA und PDB,
  • Observability,
  • Migrationstests,
  • Cutover- und Rollback-Plan,
  • Migration Decisions,
  • Risikoübersicht,
  • Before/After-Mapping-Vorbereitung,
  • Changelog-V2-Vorbereitung.

077. Lernbuch-Artefakte und Ausgabeformate
Kapitel 07 7. Lernbuch-Artefakte und Ausgabeformate Kompakter Themen-Input als Orientierung zum Abschnitt

Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Lernbuch-Artefakte und Ausgabeformate in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Ziel dieser Runde

Diese Runde erzeugt die ersten lesbaren Lernbuch-Artefakte aus dem bisherigen Rohmaterial:

  • konsolidiertes Markdown-Lernbuch masterbook.md
  • PC-HTML-Version pc.html
  • Mobile/iPhone-Safari-taugliche HTML-Version mobile.html
  • PC-PDF pc.pdf
  • Mobile-PDF mobile.pdf
  • SVG-/HTML-/PDF-Prüfnotizen

Die HTML-Versionen halten die iPhone-/Safari-Regeln ein:

  • keine externen Bibliotheken
  • keine CDN-Links
  • keine externen Bilder
  • Navigation über normale Ankerlinks
  • wichtige Inhalte ohne JavaScript erreichbar
  • Lösungen und Zusatzbereiche über details / summary
  • SVG-Grafiken direkt inline eingebettet
  • keine Pflicht-Buttons
  • Zurück/Weiter als echte Links

Konsolidierte Lernbuch-Struktur

  1. Projektüberblick und Zielbild
  2. Legacy-Systemlandschaft
  3. Zusatzsysteme und Integrationsgrenzen
  4. Legacy-Ausgangssystem vor Refactoring
  5. Monster Method processClaimDecision(...)
  6. Refactoring-Safety-Net
  7. Schrittweise Zerlegung der Monster Method
  8. Moderne modulare Maven-Zielarchitektur
  9. Ports & Adapters / Hexagonale Architektur
  10. SOAP-Kompatibilität und REST/OpenAPI-Erweiterung
  11. OpenShift-Migration
  12. Migrationstests, Risiken und Rollback
  13. Artefakt- und Qualitätsstrategie
  14. 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:

  1. Gesamt-Systemlandschaft
  2. Legacy-Architektur vor Refactoring
  3. Monster Method Zerlegung
  4. Ports & Adapters / Hexagonale Architektur
  5. Workflow / State Machine
  6. Outbox / Messaging Flow
  7. Security-Migration
  8. OpenShift Zielarchitektur
  9. Before/After-Mapping-Grafik
  10. Refactoring Roadmap

Beispiel: stabile HTML-Lösung ohne Pflicht-JavaScript

CODE
<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

CODE
<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

CODE
package com.seb4u.demo.claims.application;

public final class ProcessClaimDecisionHandler {
    private final ClaimRepository claimRepository;
    private final ClaimTransitionPolicy transitionPolicy;
    private final DocumentVerificationPort documentVerificationPort;
    private final FraudRiskPort fraudRiskPort;
    private final CoveragePort coveragePort;
    private final PaymentReleasePort paymentReleasePort;
    private final AuditLogPort auditLogPort;

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Claim claim = claimRepository.getRequired(command.claimId());
        transitionPolicy.assertAllowed(claim.status(), command.decisionType());
        documentVerificationPort.verifyRequiredDocuments(command.claimId());
        fraudRiskPort.assessRisk(command.claimId());
        coveragePort.checkCoverage(command.claimId());
        paymentReleasePort.preparePaymentIfRequired(command.claimId(), command.decisionType());
        auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
        return ProcessClaimDecisionResult.accepted(command.claimId());
    }
}
088. Änderungsdokumentation und Mapping
Kapitel 08 8. Änderungsdokumentation und Mapping Kompakter Themen-Input als Orientierung zum Abschnitt

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
Vertiefung Refactoring-Phasen

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
Vertiefung Before/After-Mapping

Before/After-Mapping

Verständnis-Skizze Before / After Mapping
Legacy-Bausteine werden systematisch auf moderne Zielmodule abgebildet.
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

CODE
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

CODE
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

CODE
package com.seb4u.demo.claims.migrationtests;

class LegacyVsModularDecisionComparisonTest {
    @Test
    void approvePayment_keepsLegacyVisibleOutcome() {
        LegacyDecisionSnapshot legacy = legacyRunner.run("CLAIM-1001", "APPROVE_PAYMENT");
        ProcessClaimDecisionResult modern = modernHandler.handle(commandFor("CLAIM-1001", APPROVE_PAYMENT));
        assertThat(modern.status()).isEqualTo(legacy.status());
        assertThat(modern.auditCodes()).containsExactlyElementsOf(legacy.auditCodes());
    }
}
099. Anhang A - Refactoring Phases
Kapitel 09 9. Anhang A - Refactoring Phases Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeRefactoring-Phasen als Arbeitsfluss
Vom Verstehen bis zur betrieblichen Absicherung.

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

CODE
01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/
Phase 01 – Inventory und Characterization

Ziel: Systeme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisieren

Änderung: Keine Änderung am Produktionscode

Sicherheitsnetz: Inventory-Dokument, Golden-Master-Snapshot

Risiko: Falsche Systemannahmen

Phase 02 – Systemlandschaft und Zusatzsysteme erfassen

Ziel: Partner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreiben

Änderung: Kontextgrenzen sichtbar

Sicherheitsnetz: System Landscape Map

Risiko: verdeckte Kopplungen

Phase 03 – Monster Method verstehen

Ziel: processClaimDecision(...) absichern und fachliche Schritte markieren

Änderung: Seams, Logging, Testdaten

Sicherheitsnetz: Characterization Test

Risiko: Tests bilden Bugs mit ab

Phase 04 – Facade und Command extrahieren

Ziel: Input in Command Object und Result Object überführen

Änderung: Command Handler

Sicherheitsnetz: Handler Test

Risiko: Transaktionskontext prüfen

Phase 05 – Statuslogik in State/Policy überführen

Ziel: if/else durch ClaimTransitionPolicy ersetzen

Änderung: State Pattern, Policy Object

Sicherheitsnetz: State Transition Test

Risiko: Sonderstatus fehlen

Phase 06 – Coverage/Fraud/Document/Payment Ports extrahieren

Ziel: direkte SOAP/File/Payment Calls ablösen

Änderung: Ports & Adapters

Sicherheitsnetz: Adapter Contract Tests

Risiko: Fehlersemantik angleichen

Phase 07 – Customer und Identity Ports extrahieren

Ziel: Stammdaten und Rollenprüfung isolieren

Änderung: ACL, AuthorizationPolicy

Sicherheitsnetz: Role Mapping Test

Risiko: Rollenmigration

Phase 08 – Workflow Orchestrator einführen

Ziel: Ablauf koordinieren, Fachlogik in Use Cases halten

Änderung: Workflow Orchestrator

Sicherheitsnetz: Workflow Scenario Test

Risiko: God-Orchestrator vermeiden

Phase 09 – Audit Trail abstrahieren

Ziel: fachliche Audit Events statt technischer Logs

Änderung: AuditLogPort

Sicherheitsnetz: Audit Snapshot Test

Risiko: PII/Compliance

Phase 10 – Search, Notification, Reporting trennen

Ziel: seiteneffektarme Boundaries schaffen

Änderung: Read Model, Outbox

Sicherheitsnetz: Search/Notification Tests

Risiko: Eventual consistency

Phase 11 – Outbox und Idempotency einführen

Ziel: JMS/Payment sicher wiederholbar machen

Änderung: Outbox Pattern, Idempotency Key

Sicherheitsnetz: Replay/Idempotency Tests

Risiko: Doppelte externe Effekte

Phase 12 – Batch und SLA trennen

Ziel: TimerBeans in Batch Worker und Policy überführen

Änderung: Batch Worker, Lease Lock

Sicherheitsnetz: Batch Lease Test

Risiko: Doppelte Jobs

Phase 13 – Portal und Backoffice entkoppeln

Ziel: UI über REST/BFF schrittweise strangeln

Änderung: Strangler Fig

Sicherheitsnetz: Contract Tests

Risiko: parallele UI-Pfade

Phase 14 – OpenShift-Migration vorbereiten

Ziel: Runtime Boundary, Config, Health, Security, Observability

Änderung: Container, ConfigMap, Secret, CronJob

Sicherheitsnetz: Deployment Smoke Test

Risiko: Runtime-Unterschiede


1010. Anhang B - Essential Changes
Kapitel 10 10. Anhang B - Essential Changes Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeEssential Changes
Änderungen mit Motivation, Wirkung, Risiko und Nachweis.

Dieses Paket erklärt die wichtigsten Änderungen jeweils mit Vorher/Nachher, Pattern, Ordnern, Tests und Migrationsnutzen.

ID Änderung Vorher Nachher Pattern / Konzept Relevante Ordner Test / Sicherheitsnetz Migrationsnutzen
CLG-001 Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht Nur Zielarchitektur diskutiert /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar Architecture Baseline docs, legacy-websphere-ear Characterization Tests Migration startet mit belegbarem Ist-Zustand
CLG-002 Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik claim-domain und claim-application enthalten Geschäftsregeln Hexagonal Architecture claim-domain, claim-application Unit Tests, ArchUnit Fachlogik ohne WebSphere testbar
CLG-003 processClaimDecision(...) in fachliche Schritte zerlegt Eine große Methode entscheidet, prüft, speichert, publiziert Command Handler + Workflow Orchestrator + Ports Command Handler, Orchestrator claim-application, claim-workflow Golden Master, Handler Tests Schrittweise Migration möglich
CLG-004 Claim Statuslogik aus if/else-Ketten herausgezogen Statusübergänge in verschachtelten if/else ClaimState + ClaimTransitionPolicy State Pattern, Policy Object claim-domain State Transition Tests Regeln werden sichtbar und änderbar
CLG-005 Workflow Orchestrator eingeführt Workflow versteckt im EJB ClaimDecisionWorkflowOrchestrator koordiniert Schritte Workflow Orchestrator claim-workflow Workflow Scenario Tests Ablauf kann später als Service laufen
CLG-006 Ports & Adapters eingeführt Direkte SOAP/JMS/JDBC/File-Aufrufe Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter Ports & Adapters claim-application, claim-infrastructure Adapter Contract Tests Technik austauschbar
CLG-007 SOAP Boundary kompatibel gehalten Partner hängen direkt an Legacy SOAP Endpoint claim-soap-api übersetzt SOAP auf Use Cases Facade, Anti-Corruption Layer claim-soap-api SOAP Compatibility Tests Partner müssen nicht sofort migrieren
CLG-008 REST/OpenAPI ergänzt Keine moderne API für neue UIs claim-rest-api + claim-openapi API Facade claim-rest-api, claim-openapi REST Smoke Tests BFF/Frontend-Strangler möglich
CLG-009 Document Adapter eingeführt File Shares hart verdrahtet DocumentStoragePort + FileShareDocumentAdapter Adapter, ACL claim-document Adapter Tests später Object Storage möglich
CLG-010 Fraud Risk Gateway entkoppelt Direkter SOAP Client im Fachfluss FraudRiskPort + FraudRiskSoapAdapter Anti-Corruption Layer claim-infrastructure Contract Test, Timeout Test Fehler-/Retry-Politik steuerbar
CLG-011 Policy Coverage über CoveragePort abstrahiert Coverage SOAP/Stored Procedure direkt genutzt CoveragePort + CoverageDecision Specification, Policy Object claim-domain, claim-infrastructure Coverage Tests Alte Tariflogik isoliert
CLG-012 Payment Release entkoppelt Zahlung direkt in JTA-Fluss ausgelöst PaymentReleasePort + Outbox/Compensation Outbox, Idempotency claim-payment Idempotency Tests Zahlungen sicherer migrierbar
CLG-013 NotificationPort modernisiert EJB sendet JMS/SOAP/E-Mail direkt NotificationPort + NotificationAdapter Adapter, Outbox claim-notification DLQ/Retry Tests Benachrichtigung entkoppelt
CLG-014 SLA & Escalation Scheduler getrennt EJB Timer mit DB-Seiteneffekten EscalationBatchJob + EscalationPolicy Batch Worker, Policy Object claim-batch Batch Lease Tests CronJob auf OpenShift möglich
CLG-015 Audit & Compliance Boundary getrennt Technische Logs und Stored Procedures vermischt AuditLogPort + Reporting Read Model Audit Event, Read Model claim-audit Audit Snapshot Tests Compliance nachvollziehbar
CLG-016 Search Indexer über ClaimSearchPort abstrahiert Oracle LIKE Queries in DAO/Backoffice ClaimSearchPort + SearchIndexAdapter Read Model, Adapter claim-search Search Adapter Tests später OpenSearch möglich
CLG-017 Customer Master Data entkoppelt Direkter SOAP Client und alte Kundennummern CustomerMasterDataPort + CustomerSnapshot ACL, Value Object claim-infrastructure Customer Contract Tests Dubletten/Inkonsistenz isoliert
CLG-018 Identity & Role Management abstrahiert JAAS/WebSphere-Rollen hart kodiert SecurityContextPort + AuthorizationPolicy Policy Object, Role Mapping claim-application, claim-infrastructure Role Mapping Tests IdP-Migration vorbereitet
CLG-019 Outbox und Idempotency eingeführt JMS/Payment im gleichen Fluss ohne Wiederholschutz OutboxEvent + IdempotencyKey Outbox Pattern claim-infrastructure, claim-payment Outbox Publisher Tests robuster bei Pod-Restarts
CLG-020 OpenShift-/Runtime-Migration vorbereitet EAR manuell auf WebSphere deployt Container, Deployment, Route, ConfigMap, Secret Runtime Boundary migration_to_openshift Deployment Smoke Tests Automatisierbare Migration

1111. Anhang C - Changelog V2
Kapitel 11 11. Anhang C - Changelog V2 Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeChangelog V2 Prüfkette
Jede Änderung bleibt auditierbar und testbar.

Dieses Dokument sammelt die wesentlichen Architektur-, Refactoring- und Migrationsänderungen der Runde 7.

ID Kategorie Änderung Vorher Nachher Pattern / Konzept Module Tests Migrationsnutzen Risiko
CLG-001 Legacy sichtbar gemacht Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht Nur Zielarchitektur diskutiert /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar Architecture Baseline docs, legacy-websphere-ear Characterization Tests Migration startet mit belegbarem Ist-Zustand Alte Annahmen können falsch sein
CLG-002 Fachlogik Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik claim-domain und claim-application enthalten Geschäftsregeln Hexagonal Architecture claim-domain, claim-application Unit Tests, ArchUnit Fachlogik ohne WebSphere testbar Transaktionsgrenzen neu bewerten
CLG-003 Monster Method processClaimDecision(...) in fachliche Schritte zerlegt Eine große Methode entscheidet, prüft, speichert, publiziert Command Handler + Workflow Orchestrator + Ports Command Handler, Orchestrator claim-application, claim-workflow Golden Master, Handler Tests Schrittweise Migration möglich Verhalten muss identisch bleiben
CLG-004 Statuslogik Claim Statuslogik aus if/else-Ketten herausgezogen Statusübergänge in verschachtelten if/else ClaimState + ClaimTransitionPolicy State Pattern, Policy Object claim-domain State Transition Tests Regeln werden sichtbar und änderbar Sonderfälle übersehen
CLG-005 Workflow Workflow Orchestrator eingeführt Workflow versteckt im EJB ClaimDecisionWorkflowOrchestrator koordiniert Schritte Workflow Orchestrator claim-workflow Workflow Scenario Tests Ablauf kann später als Service laufen Zu viel Logik im Orchestrator vermeiden
CLG-006 Ports Ports & Adapters eingeführt Direkte SOAP/JMS/JDBC/File-Aufrufe Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter Ports & Adapters claim-application, claim-infrastructure Adapter Contract Tests Technik austauschbar Port-Schnittstellen nicht zu technisch machen
CLG-007 SOAP SOAP Boundary kompatibel gehalten Partner hängen direkt an Legacy SOAP Endpoint claim-soap-api übersetzt SOAP auf Use Cases Facade, Anti-Corruption Layer claim-soap-api SOAP Compatibility Tests Partner müssen nicht sofort migrieren Schema-Kompatibilität prüfen
CLG-008 REST REST/OpenAPI ergänzt Keine moderne API für neue UIs claim-rest-api + claim-openapi API Facade claim-rest-api, claim-openapi REST Smoke Tests BFF/Frontend-Strangler möglich Doppelte API-Semantik vermeiden
CLG-009 Document Document Adapter eingeführt File Shares hart verdrahtet DocumentStoragePort + FileShareDocumentAdapter Adapter, ACL claim-document Adapter Tests später Object Storage möglich Dateipfade/Permissions
CLG-010 Fraud Fraud Risk Gateway entkoppelt Direkter SOAP Client im Fachfluss FraudRiskPort + FraudRiskSoapAdapter Anti-Corruption Layer claim-infrastructure Contract Test, Timeout Test Fehler-/Retry-Politik steuerbar Fachliche vs technische Fehler trennen
CLG-011 Coverage Policy Coverage über CoveragePort abstrahiert Coverage SOAP/Stored Procedure direkt genutzt CoveragePort + CoverageDecision Specification, Policy Object claim-domain, claim-infrastructure Coverage Tests Alte Tariflogik isoliert Regelabweichungen dokumentieren
CLG-012 Payment Payment Release entkoppelt Zahlung direkt in JTA-Fluss ausgelöst PaymentReleasePort + Outbox/Compensation Outbox, Idempotency claim-payment Idempotency Tests Zahlungen sicherer migrierbar Doppelauszahlung vermeiden
CLG-013 Notification NotificationPort modernisiert EJB sendet JMS/SOAP/E-Mail direkt NotificationPort + NotificationAdapter Adapter, Outbox claim-notification DLQ/Retry Tests Benachrichtigung entkoppelt Templates versionieren
CLG-014 SLA SLA & Escalation Scheduler getrennt EJB Timer mit DB-Seiteneffekten EscalationBatchJob + EscalationPolicy Batch Worker, Policy Object claim-batch Batch Lease Tests CronJob auf OpenShift möglich Doppelverarbeitung verhindern
CLG-015 Audit Audit & Compliance Boundary getrennt Technische Logs und Stored Procedures vermischt AuditLogPort + Reporting Read Model Audit Event, Read Model claim-audit Audit Snapshot Tests Compliance nachvollziehbar Datenschutz/PII beachten
CLG-016 Search Search Indexer über ClaimSearchPort abstrahiert Oracle LIKE Queries in DAO/Backoffice ClaimSearchPort + SearchIndexAdapter Read Model, Adapter claim-search Search Adapter Tests später OpenSearch möglich Indexkonsistenz
CLG-017 Customer Customer Master Data entkoppelt Direkter SOAP Client und alte Kundennummern CustomerMasterDataPort + CustomerSnapshot ACL, Value Object claim-infrastructure Customer Contract Tests Dubletten/Inkonsistenz isoliert Datenqualität bleibt Thema
CLG-018 Identity Identity & Role Management abstrahiert JAAS/WebSphere-Rollen hart kodiert SecurityContextPort + AuthorizationPolicy Policy Object, Role Mapping claim-application, claim-infrastructure Role Mapping Tests IdP-Migration vorbereitet Rollenmatrix abgleichen
CLG-019 Outbox Outbox und Idempotency eingeführt JMS/Payment im gleichen Fluss ohne Wiederholschutz OutboxEvent + IdempotencyKey Outbox Pattern claim-infrastructure, claim-payment Outbox Publisher Tests robuster bei Pod-Restarts Eventual Consistency erklären
CLG-020 OpenShift OpenShift-/Runtime-Migration vorbereitet EAR manuell auf WebSphere deployt Container, Deployment, Route, ConfigMap, Secret Runtime Boundary migration_to_openshift Deployment Smoke Tests Automatisierbare Migration Runtime-Unterschiede prüfen

Ausführliche Hinweise

CLG-001 – Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht
  • Vorher: Nur Zielarchitektur diskutiert
  • Nachher: /before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbar
  • Konzept: Architecture Baseline
  • Betroffene Module: docs, legacy-websphere-ear
  • Sicherheitsnetz: Characterization Tests
  • Migrationsnutzen: Migration startet mit belegbarem Ist-Zustand
  • Risiko / Achtung: Alte Annahmen können falsch sein
CLG-002 – Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen
  • Vorher: EJB enthält Fach-, Transaktions-, SOAP-, JMS- und DAO-Logik
  • Nachher: claim-domain und claim-application enthalten Geschäftsregeln
  • Konzept: Hexagonal Architecture
  • Betroffene Module: claim-domain, claim-application
  • Sicherheitsnetz: Unit Tests, ArchUnit
  • Migrationsnutzen: Fachlogik ohne WebSphere testbar
  • Risiko / Achtung: Transaktionsgrenzen neu bewerten
CLG-003 – processClaimDecision(...) in fachliche Schritte zerlegt
  • Vorher: Eine große Methode entscheidet, prüft, speichert, publiziert
  • Nachher: Command Handler + Workflow Orchestrator + Ports
  • Konzept: Command Handler, Orchestrator
  • Betroffene Module: claim-application, claim-workflow
  • Sicherheitsnetz: Golden Master, Handler Tests
  • Migrationsnutzen: Schrittweise Migration möglich
  • Risiko / Achtung: Verhalten muss identisch bleiben
CLG-004 – Claim Statuslogik aus if/else-Ketten herausgezogen
  • Vorher: Statusübergänge in verschachtelten if/else
  • Nachher: ClaimState + ClaimTransitionPolicy
  • Konzept: State Pattern, Policy Object
  • Betroffene Module: claim-domain
  • Sicherheitsnetz: State Transition Tests
  • Migrationsnutzen: Regeln werden sichtbar und änderbar
  • Risiko / Achtung: Sonderfälle übersehen
CLG-005 – Workflow Orchestrator eingeführt
  • Vorher: Workflow versteckt im EJB
  • Nachher: ClaimDecisionWorkflowOrchestrator koordiniert Schritte
  • Konzept: Workflow Orchestrator
  • Betroffene Module: claim-workflow
  • Sicherheitsnetz: Workflow Scenario Tests
  • Migrationsnutzen: Ablauf kann später als Service laufen
  • Risiko / Achtung: Zu viel Logik im Orchestrator vermeiden
CLG-006 – Ports & Adapters eingeführt
  • Vorher: Direkte SOAP/JMS/JDBC/File-Aufrufe
  • Nachher: Fachlogik ruft Ports auf, Infrastruktur implementiert Adapter
  • Konzept: Ports & Adapters
  • Betroffene Module: claim-application, claim-infrastructure
  • Sicherheitsnetz: Adapter Contract Tests
  • Migrationsnutzen: Technik austauschbar
  • Risiko / Achtung: Port-Schnittstellen nicht zu technisch machen
CLG-007 – SOAP Boundary kompatibel gehalten
  • Vorher: Partner hängen direkt an Legacy SOAP Endpoint
  • Nachher: claim-soap-api übersetzt SOAP auf Use Cases
  • Konzept: Facade, Anti-Corruption Layer
  • Betroffene Module: claim-soap-api
  • Sicherheitsnetz: SOAP Compatibility Tests
  • Migrationsnutzen: Partner müssen nicht sofort migrieren
  • Risiko / Achtung: Schema-Kompatibilität prüfen
CLG-008 – REST/OpenAPI ergänzt
  • Vorher: Keine moderne API für neue UIs
  • Nachher: claim-rest-api + claim-openapi
  • Konzept: API Facade
  • Betroffene Module: claim-rest-api, claim-openapi
  • Sicherheitsnetz: REST Smoke Tests
  • Migrationsnutzen: BFF/Frontend-Strangler möglich
  • Risiko / Achtung: Doppelte API-Semantik vermeiden
CLG-009 – Document Adapter eingeführt
  • Vorher: File Shares hart verdrahtet
  • Nachher: DocumentStoragePort + FileShareDocumentAdapter
  • Konzept: Adapter, ACL
  • Betroffene Module: claim-document
  • Sicherheitsnetz: Adapter Tests
  • Migrationsnutzen: später Object Storage möglich
  • Risiko / Achtung: Dateipfade/Permissions
CLG-010 – Fraud Risk Gateway entkoppelt
  • Vorher: Direkter SOAP Client im Fachfluss
  • Nachher: FraudRiskPort + FraudRiskSoapAdapter
  • Konzept: Anti-Corruption Layer
  • Betroffene Module: claim-infrastructure
  • Sicherheitsnetz: Contract Test, Timeout Test
  • Migrationsnutzen: Fehler-/Retry-Politik steuerbar
  • Risiko / Achtung: Fachliche vs technische Fehler trennen
CLG-011 – Policy Coverage über CoveragePort abstrahiert
  • Vorher: Coverage SOAP/Stored Procedure direkt genutzt
  • Nachher: CoveragePort + CoverageDecision
  • Konzept: Specification, Policy Object
  • Betroffene Module: claim-domain, claim-infrastructure
  • Sicherheitsnetz: Coverage Tests
  • Migrationsnutzen: Alte Tariflogik isoliert
  • Risiko / Achtung: Regelabweichungen dokumentieren
CLG-012 – Payment Release entkoppelt
  • Vorher: Zahlung direkt in JTA-Fluss ausgelöst
  • Nachher: PaymentReleasePort + Outbox/Compensation
  • Konzept: Outbox, Idempotency
  • Betroffene Module: claim-payment
  • Sicherheitsnetz: Idempotency Tests
  • Migrationsnutzen: Zahlungen sicherer migrierbar
  • Risiko / Achtung: Doppelauszahlung vermeiden
CLG-013 – NotificationPort modernisiert
  • Vorher: EJB sendet JMS/SOAP/E-Mail direkt
  • Nachher: NotificationPort + NotificationAdapter
  • Konzept: Adapter, Outbox
  • Betroffene Module: claim-notification
  • Sicherheitsnetz: DLQ/Retry Tests
  • Migrationsnutzen: Benachrichtigung entkoppelt
  • Risiko / Achtung: Templates versionieren
CLG-014 – SLA & Escalation Scheduler getrennt
  • Vorher: EJB Timer mit DB-Seiteneffekten
  • Nachher: EscalationBatchJob + EscalationPolicy
  • Konzept: Batch Worker, Policy Object
  • Betroffene Module: claim-batch
  • Sicherheitsnetz: Batch Lease Tests
  • Migrationsnutzen: CronJob auf OpenShift möglich
  • Risiko / Achtung: Doppelverarbeitung verhindern
CLG-015 – Audit & Compliance Boundary getrennt
  • Vorher: Technische Logs und Stored Procedures vermischt
  • Nachher: AuditLogPort + Reporting Read Model
  • Konzept: Audit Event, Read Model
  • Betroffene Module: claim-audit
  • Sicherheitsnetz: Audit Snapshot Tests
  • Migrationsnutzen: Compliance nachvollziehbar
  • Risiko / Achtung: Datenschutz/PII beachten
CLG-016 – Search Indexer über ClaimSearchPort abstrahiert
  • Vorher: Oracle LIKE Queries in DAO/Backoffice
  • Nachher: ClaimSearchPort + SearchIndexAdapter
  • Konzept: Read Model, Adapter
  • Betroffene Module: claim-search
  • Sicherheitsnetz: Search Adapter Tests
  • Migrationsnutzen: später OpenSearch möglich
  • Risiko / Achtung: Indexkonsistenz
CLG-017 – Customer Master Data entkoppelt
  • Vorher: Direkter SOAP Client und alte Kundennummern
  • Nachher: CustomerMasterDataPort + CustomerSnapshot
  • Konzept: ACL, Value Object
  • Betroffene Module: claim-infrastructure
  • Sicherheitsnetz: Customer Contract Tests
  • Migrationsnutzen: Dubletten/Inkonsistenz isoliert
  • Risiko / Achtung: Datenqualität bleibt Thema
CLG-018 – Identity & Role Management abstrahiert
  • Vorher: JAAS/WebSphere-Rollen hart kodiert
  • Nachher: SecurityContextPort + AuthorizationPolicy
  • Konzept: Policy Object, Role Mapping
  • Betroffene Module: claim-application, claim-infrastructure
  • Sicherheitsnetz: Role Mapping Tests
  • Migrationsnutzen: IdP-Migration vorbereitet
  • Risiko / Achtung: Rollenmatrix abgleichen
CLG-019 – Outbox und Idempotency eingeführt
  • Vorher: JMS/Payment im gleichen Fluss ohne Wiederholschutz
  • Nachher: OutboxEvent + IdempotencyKey
  • Konzept: Outbox Pattern
  • Betroffene Module: claim-infrastructure, claim-payment
  • Sicherheitsnetz: Outbox Publisher Tests
  • Migrationsnutzen: robuster bei Pod-Restarts
  • Risiko / Achtung: Eventual Consistency erklären
CLG-020 – OpenShift-/Runtime-Migration vorbereitet
  • Vorher: EAR manuell auf WebSphere deployt
  • Nachher: Container, Deployment, Route, ConfigMap, Secret
  • Konzept: Runtime Boundary
  • Betroffene Module: migration_to_openshift
  • Sicherheitsnetz: Deployment Smoke Tests
  • Migrationsnutzen: Automatisierbare Migration
  • Risiko / Achtung: Runtime-Unterschiede prüfen

1212. Anhang D - Before/After Mapping
Kapitel 12 12. Anhang D - Before/After Mapping Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeBefore/After-Mapping
Legacy-Verantwortungen werden auf moderne Zielmodule abgebildet.

Mapping von Legacy-Dateien und Legacy-Modulen auf moderne Dateien, Module und Konzepte.

Legacy-Datei / Modul Neue Datei / neues Modul Grund Pattern / Architekturkonzept Test / Sicherheitsnetz Migrationsnutzen
LegacyClaimFacadeBean.java ProcessClaimDecisionHandler.java Monster Method zerlegt Command Handler, Application Service Golden Master + Handler Test Fachlogik kann unabhängig von WebSphere getestet und migriert werden
LegacyClaimStatusIfElse.java ClaimState.java + ClaimTransitionPolicy.java Statuslogik aus if/else-Ketten herausgezogen State Pattern, Policy Object State Transition Test Workflow-Regeln werden verständlich und testbar
LegacyDocumentShareClient.java DocumentStoragePort.java + FileShareDocumentAdapter.java File Share entkoppelt Port, Adapter, Anti-Corruption Layer Adapter Test Dokumentenablage kann später ersetzt werden
LegacyPartnerPortalServlet.java PartnerPortalRestController.java / PartnerPortalBffAdapter.java Partner Portal vom EJB getrennt Strangler UI, BFF Adapter Portal Contract Test Externes Portal kann schrittweise modernisiert werden
InternalClaimsBackofficeBean.java ClaimBackofficeUseCase.java Backoffice-Logik aus JSP/EJB herausgezogen Use Case, Application Service Backoffice Use Case Test Interne UI kann später Angular/React nutzen
FraudRiskSoapClient.java FraudRiskPort.java + FraudRiskSoapAdapter.java Fraud SOAP entkoppelt Port, Adapter, ACL Fraud Contract Test Timeout/Retry/Circuit Breaker möglich
PolicyCoverageSoapClient.java CoveragePort.java + CoverageSoapAdapter.java Coverage-System isoliert Specification, Policy Object, Adapter Coverage Decision Test Alte Tariflogik kontrolliert migrierbar
PaymentReleaseSoapClient.java PaymentReleasePort.java + PaymentReleaseSoapAdapter.java Payment-Freigabe entkoppelt Port, Adapter, Idempotency Payment Idempotency Test Zahlungsrisiken werden reduziert
LegacyNotificationSenderBean.java NotificationPort.java + NotificationAdapter.java Benachrichtigung entkoppelt Outbox, Adapter Notification Retry Test DLQ/Retry auf Plattformebene möglich
SlaEscalationTimerBean.java EscalationBatchJob.java + EscalationPolicy.java EJB Timer ersetzt Batch Worker, Policy Object, Lease Lock Batch Lease Test OpenShift CronJob möglich
AuditReportStoredProcedureDao.java AuditLogPort.java + AuditReportingAdapter.java Audit/Reporting getrennt Audit Event, Reporting Read Model Audit Trail Test Compliance-Berichte werden fachlich nachvollziehbar
LegacyClaimSearchDao.java ClaimSearchPort.java + SearchIndexAdapter.java DB-LIKE-Suche entkoppelt Read Model, Adapter Search Adapter Test später Elasticsearch/OpenSearch möglich
CustomerMasterDataSoapClient.java CustomerMasterDataPort.java + CustomerMasterDataSoapAdapter.java Customer-Stammdaten entkoppelt ACL, Snapshot Value Object Customer Contract Test Datenqualitätsprobleme werden isoliert
JaasRoleChecker.java SecurityContextPort.java + AuthorizationPolicy.java Rollenprüfung zentralisiert Policy Object, Role Mapping Security Role Mapping Test OpenShift/IdP-Migration vorbereitet
LegacyClaimEventPublisher.java OutboxEvent.java + OutboxPublisherWorker.java Direkte JMS-Publizierung abgelöst Outbox Pattern Outbox Replay Test robust gegen Transaktions- und Pod-Fehler
LegacyClaimDao.java ClaimRepository.java + JpaClaimRepository.java DAO und Fachmodell getrennt Repository, Data Mapper Repository Integration Test DB-Zugriff ist austauschbar
ClaimDecisionSoapEndpoint.java ClaimDecisionSoapEndpoint.java + ProcessClaimDecisionHandler.java SOAP bleibt, delegiert aber nur Facade, ACL SOAP Compatibility IT Partner bleiben kompatibel
LegacyAuditLogger.java AuditLogPort.java + StructuredAuditLogAdapter.java versteckte Auditlogik sichtbar gemacht Port, Structured Logging Audit Snapshot Test Audit kann zentral ausgewertet werden

Didaktische Lesart

Das Mapping ist nicht nur eine Datei-Verschiebung. Jede Zeile zeigt eine Architekturentscheidung: Fachlogik wird aus technischen Schichten herausgelöst, technische Adapter werden austauschbar und Migration wird testbar.


1313. Anhang E - Migration Decisions
Kapitel 13 13. Anhang E - Migration Decisions Kompakter Themen-Input als Orientierung zum Abschnitt

Dieses Hauptkapitel bündelt das zugehörige Rohmaterial zum Thema Anhang E - Migration Decisions in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

MIGRATION_DECISIONS

  • WebSphere Traditional wird nicht 1:1 containerisiert, sondern über Runtime Boundaries schrittweise ersetzt.
  • SOAP bleibt als Kompatibilitätsgrenze erhalten.
  • REST/OpenAPI wird als neue Erweiterungsgrenze ergänzt.
  • JNDI-Konfiguration wird durch externe Konfiguration via ConfigMap/Secret ersetzt.
  • EJB Timer werden zu OpenShift CronJobs mit Lease Lock.
  • JMS-Seiteneffekte werden über Outbox und Idempotency abgesichert.
  • FileShare bleibt zunächst Adapter, später Object Storage möglich.
  • JAAS/LDAP wird über SecurityContextPort und AuthorizationPolicy gekapselt.

1414. Anhang F - Risks
Kapitel 14 14. Anhang F - Risks Kompakter Themen-Input als Orientierung zum Abschnitt

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.

Vertiefung RISKS

RISKS

FachskizzeRisk Register
Risiken werden mit Ursache, Gegenmaßnahme und Nachweis geführt.
Risiko Beschreibung Gegenmaßnahme
Verhaltensänderung Refactoring verändert unbemerkt Legacy-Verhalten Golden Master und Characterization Tests
Doppelte Zahlungen Payment-Retry löst extern mehrfach aus IdempotencyKey, Outbox, Contract Tests
Rollenfehler JAAS-Gruppen werden falsch gemappt Role Mapping Test, Vier-Augen-Prüfung
Batch-Duplikate mehrere Pods starten denselben Job Lease Lock, CronJob Policy
Datenqualität Customer Master Data bleibt inkonsistent CustomerSnapshot, ACL, Qualitätsreports

1515. Anhang G - Runde 07 Codebeispiele
Kapitel 15 15. Anhang G - Runde 07 Codebeispiele Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeCodebeispiele lesen
Code zeigt Grenzen: Command, Policy, Port, Handler und Test.

State/Policy statt if/else

CODE
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

CODE
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

CODE
package com.seb4u.demo.claims.migrationtests;

class LegacyVsModularDecisionComparisonTest {
    @Test
    void approvePayment_keepsLegacyVisibleOutcome() {
        LegacyDecisionSnapshot legacy = legacyRunner.run("CLAIM-1001", "APPROVE_PAYMENT");
        ProcessClaimDecisionResult modern = modernHandler.handle(commandFor("CLAIM-1001", APPROVE_PAYMENT));
        assertThat(modern.status()).isEqualTo(legacy.status());
        assertThat(modern.auditCodes()).containsExactlyElementsOf(legacy.auditCodes());
    }
}

1616. Anhang H - Großes Glossar
Kapitel 16 16. Anhang H - Großes Glossar Kompakter Themen-Input als Orientierung zum Abschnitt

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

FachskizzeGlossar als Lernanker
Begriffe werden nach Kontext und Projektstelle verstanden.

Legacy-System

Erklärung: Gewachsene Anwendung mit historischer Architektur, alten Laufzeiten, manuellen Abläufen und oft unklaren Verantwortlichkeiten.

Beispiel im Lernprojekt: WebSphere Traditional mit EAR, WAR, EJB, SOAP, JMS, Oracle und File Shares.

Warum wichtig: Macht sichtbar, was zuerst abgesichert und verstanden werden muss.

IBM WebSphere Traditional

Erklärung: Klassische Java-Enterprise-Laufzeit mit serverseitiger Konfiguration, JNDI, Deployment-Deskriptoren und EAR-Deployments.

Beispiel im Lernprojekt: Ausgangspunkt der Migration.

Warum wichtig: Trennung von Laufzeitabhängigkeiten und Fachlogik.

EAR

Erklärung: Enterprise Archive, das mehrere Java-EE-Module wie WAR und EJB-JAR gemeinsam deployt.

Beispiel im Lernprojekt: legacy-websphere-ear.

Warum wichtig: Module können später getrennt gebaut, getestet und deployt werden.

WAR

Erklärung: Web Archive für Weboberflächen, Servlets, JSPs und webnahe Ressourcen.

Beispiel im Lernprojekt: Partner Portal und Internal Backoffice.

Warum wichtig: UI-Schicht von Fachlogik trennen.

EJB-JAR

Erklärung: Archiv für Enterprise JavaBeans, häufig mit Transaktionen, Security, Scheduler und Fachlogik vermischt.

Beispiel im Lernprojekt: LegacyClaimFacadeBean.

Warum wichtig: Use Cases aus Container-Code herausziehen.

EJB

Erklärung: Enterprise JavaBean. Im Legacy-System oft Ort von Transaktion, Integration, Security und Fachentscheidung zugleich.

Beispiel im Lernprojekt: processClaimDecision in einem Session Bean.

Warum wichtig: Fachlogik wird testbar, wenn sie in Application Services wandert.

JSP

Erklärung: Serverseitige View-Technologie, bei der HTML und Java-nahe Logik häufig vermischt werden.

Beispiel im Lernprojekt: Backoffice-Masken und Partnerportal-Seiten.

Warum wichtig: UI kann schrittweise durch REST/BFF abgelöst werden.

Servlet

Erklärung: Java-Webkomponente für HTTP-Requests, im Legacy oft direkt mit EJBs oder SOAP-Clients gekoppelt.

Beispiel im Lernprojekt: LegacyPartnerPortalServlet.

Warum wichtig: Controller werden dünn, Use Cases werden separat.

SOAP

Erklärung: XML-basiertes Webservice-Protokoll mit festen Contracts und WSDLs.

Beispiel im Lernprojekt: createClaim, getClaimStatus, processClaimDecision.

Warum wichtig: Kompatibilität bleibt erhalten, intern wird modernisiert.

WSDL

Erklärung: Beschreibung eines SOAP-Service mit Operationen, Nachrichten und Datentypen.

Beispiel im Lernprojekt: Claims SOAP API.

Warum wichtig: Contract Tests schützen bestehende Partner.

JMS

Erklärung: Java Message Service für asynchrone Nachrichten und Events.

Beispiel im Lernprojekt: ClaimDecisionProcessedEvent an IBM MQ.

Warum wichtig: Outbox entkoppelt Eventversand von Fachtransaktion.

IBM MQ

Erklärung: Enterprise-Messaging-System, das häufig mit JMS integriert wird.

Beispiel im Lernprojekt: Legacy-Queues für Notifications, Audit oder Payment.

Warum wichtig: Adapter und Outbox reduzieren direkte Kopplung.

JTA

Erklärung: Java Transaction API für containerverwaltete Transaktionen.

Beispiel im Lernprojekt: Eine Methode schreibt Daten, sendet JMS und ruft SOAP auf.

Warum wichtig: Transaktionsgrenzen werden bewusst gemacht.

JNDI

Erklärung: Namensdienst für DataSources, Queues und Ressourcen in Application Servern.

Beispiel im Lernprojekt: java:comp/env/jdbc/ClaimsDS.

Warum wichtig: Externe Konfiguration ersetzt servergebundene Lookups.

JNDI DataSource

Erklärung: Serverseitig konfigurierte Datenbankverbindung.

Beispiel im Lernprojekt: Oracle DataSource in WebSphere.

Warum wichtig: Konfiguration wird container- und OpenShift-tauglich.

JAAS

Erklärung: Java Authentication and Authorization Service für klassische Rollen- und Sicherheitsprüfung.

Beispiel im Lernprojekt: JaasRoleChecker.

Warum wichtig: SecurityContextPort und AuthorizationPolicy ersetzen harte Prüfungen.

LDAP

Erklärung: Verzeichnisdienst für Benutzer, Gruppen und Rollen.

Beispiel im Lernprojekt: Claims-Agent, Teamlead, Partnergruppe.

Warum wichtig: Rollenmapping wird dokumentiert und testbar.

Stored Procedure

Erklärung: Datenbankprozedur, die Fachlogik, Reporting oder technische Aktionen kapseln kann.

Beispiel im Lernprojekt: SP_PROCESS_CLAIM_DECISION.

Warum wichtig: Fachlogik wird aus der Datenbank heraus identifiziert und isoliert.

Oracle LIKE Query

Erklärung: Einfache textuelle Suche mit SQL-LIKE, oft langsam und unscharf.

Beispiel im Lernprojekt: LegacyClaimSearchDao.

Warum wichtig: SearchPort ermöglicht später Search Read Model oder OpenSearch.

File Share

Erklärung: Gemeinsame Dateifreigabe für Dokumente, häufig mit harten Pfaden und schwacher Fehlerbehandlung.

Beispiel im Lernprojekt: LegacyDocumentShareClient.

Warum wichtig: DocumentStoragePort kapselt die Ablage.

Refactoring-Grundlagen

Refactoring

Erklärung: Strukturelle Verbesserung von Code, ohne das beobachtbare Verhalten absichtlich zu ändern.

Beispiel im Lernprojekt: Zerlegung von processClaimDecision.

Warum wichtig: Risikoarme Modernisierung vor Plattformwechsel.

Characterization Test

Erklärung: Test, der aktuelles Legacy-Verhalten festhält, auch wenn dieses Verhalten nicht ideal ist.

Beispiel im Lernprojekt: LegacyProcessClaimDecisionCharacterizationTest.

Warum wichtig: Sicherheitsnetz vor dem Umbau.

Golden Master Test

Erklärung: Vergleicht große Ein-/Ausgabe-Snapshots, um Verhalten über Refactoring-Schritte hinweg zu sichern.

Beispiel im Lernprojekt: GoldenMasterSnapshot.

Warum wichtig: Erkennt unbeabsichtigte Verhaltensänderungen.

Refactoring Seam

Erklärung: Kontrollierter Schnittpunkt, an dem Legacy-Code abgefangen, getestet oder schrittweise umgeleitet werden kann.

Beispiel im Lernprojekt: Wrapper um LegacyClaimFacadeBean.

Warum wichtig: Erlaubt inkrementelle Änderungen.

Monster Method

Erklärung: Sehr große Methode mit vielen Verantwortlichkeiten, Bedingungen, Seiteneffekten und Integrationen.

Beispiel im Lernprojekt: processClaimDecision(...).

Warum wichtig: Wird in Use Case, Policies, Ports und Adapter zerlegt.

Long Parameter List

Erklärung: Anti-Pattern, bei dem eine Methode sehr viele Einzelparameter benötigt.

Beispiel im Lernprojekt: processClaimDecision mit ClaimId, User, Rollen, Dokumenten, Status, Beträgen.

Warum wichtig: Command Object bündelt Eingaben.

Primitive Obsession

Erklärung: Fachliche Konzepte werden nur als String, int oder BigDecimal modelliert.

Beispiel im Lernprojekt: Status als String, Betrag als BigDecimal ohne Bedeutung.

Warum wichtig: Value Objects machen Regeln explizit.

God Class

Erklärung: Klasse mit zu vielen Verantwortlichkeiten.

Beispiel im Lernprojekt: LegacyClaimFacadeBean.

Warum wichtig: Verantwortlichkeiten werden auf Module verteilt.

Hidden Side Effect

Erklärung: Nebenwirkung, die im Code nicht klar erkennbar ist.

Beispiel im Lernprojekt: DAO speichert, JMS sendet, Audit schreibt im gleichen Pfad.

Warum wichtig: Ports und Tests machen Seiteneffekte sichtbar.

Manual Error Handling

Erklärung: Fehlerbehandlung durch verstreute try/catch-Blöcke, Flags und technische Statuscodes.

Beispiel im Lernprojekt: SOAP-Timeout wird als fachliche Ablehnung interpretiert.

Warum wichtig: Result Objects und Policies trennen Technik von Fachlichkeit.

Technical Exception as Business Flow

Erklärung: Technische Exceptions steuern fachliche Entscheidungen.

Beispiel im Lernprojekt: Timeout bedeutet angeblich Fraud Reject.

Warum wichtig: Anti-Corruption Layer übersetzt sauber.

Test Data Builder

Erklärung: Testhilfe zum lesbaren Erzeugen komplexer Testobjekte.

Beispiel im Lernprojekt: ClaimBuilder, CommandBuilder.

Warum wichtig: Tests werden kürzer und aussagekräftiger.

Fake Object

Erklärung: Einfache Testimplementierung einer Abhängigkeit.

Beispiel im Lernprojekt: FakeFraudRiskPort.

Warum wichtig: Use Cases werden ohne SOAP/DB testbar.

Spy

Erklärung: Testobjekt, das Aufrufe aufzeichnet.

Beispiel im Lernprojekt: SpyAuditLogPort.

Warum wichtig: Belegt, dass Audit-Events erzeugt wurden.

Adapter Test

Erklärung: Test für eine technische Adapterimplementierung.

Beispiel im Lernprojekt: FraudRiskSoapAdapterTest.

Warum wichtig: Sichert Mapping, Fehlerfälle und Timeouts.

Contract Test

Erklärung: Test gegen ein vereinbartes Schnittstellenverhalten.

Beispiel im Lernprojekt: SOAP Contract Test für Partner.

Warum wichtig: Schützt externe Konsumenten.

Architecture Test

Erklärung: Automatisierter Test für Architekturregeln.

Beispiel im Lernprojekt: Domain darf Infrastructure nicht importieren.

Warum wichtig: Verhindert Rückfall in enge Kopplung.

ArchUnit

Erklärung: Java-Testbibliothek zur Prüfung von Paket- und Architekturregeln.

Beispiel im Lernprojekt: ArchitectureBoundaryTest.

Warum wichtig: Macht Architekturgrenzen dauerhaft prüfbar.

Behavior Preservation

Erklärung: Erhaltung des fachlich beobachtbaren Verhaltens während intern umgebaut wird.

Beispiel im Lernprojekt: Legacy-vs-Modular Vergleichstest.

Warum wichtig: Wichtig für risikoreiches Refactoring.

Incremental Refactoring

Erklärung: Umbau in kleinen, überprüfbaren Schritten statt Big Bang.

Beispiel im Lernprojekt: Phasen 1 bis 14.

Warum wichtig: Reduziert Migrationsrisiko.

Moderne Architektur und Domain Design

Hexagonale Architektur

Erklärung: Architekturstil mit Fachkern in der Mitte und Ports/Adapters an den Rändern.

Beispiel im Lernprojekt: claim-domain, claim-application, claim-infrastructure.

Warum wichtig: Domäne wird unabhängig von Technik.

Ports & Adapters

Erklärung: Muster, bei dem Fachlogik Ports definiert und technische Adapter diese Ports implementieren.

Beispiel im Lernprojekt: CoveragePort + CoverageSoapAdapter.

Warum wichtig: Integration austauschbar und testbar.

Port

Erklärung: Fachliche Schnittstelle, die beschreibt, was die Anwendung benötigt.

Beispiel im Lernprojekt: PaymentReleasePort.

Warum wichtig: Verhindert direkte SOAP-/JDBC-Abhängigkeit in Use Cases.

Adapter

Erklärung: Technische Implementierung eines Ports.

Beispiel im Lernprojekt: FileShareDocumentAdapter.

Warum wichtig: Kapselt alte Technik und Mapping.

Anti-Corruption Layer

Erklärung: Schutzschicht zwischen alter Integration und moderner Domäne.

Beispiel im Lernprojekt: FraudRiskSoapAdapter übersetzt XML in RiskScore.

Warum wichtig: Alte Begriffe verschmutzen die neue Domäne nicht.

Domain Model

Erklärung: Fachliches Modell mit Entitäten, Value Objects, Regeln und Policies.

Beispiel im Lernprojekt: Claim, ClaimStatus, RiskScore.

Warum wichtig: Fachlichkeit wird zentral und verständlich.

Entity

Erklärung: Objekt mit Identität und Lebenszyklus.

Beispiel im Lernprojekt: Claim.

Warum wichtig: Status und Entscheidungen werden konsistent verwaltet.

Value Object

Erklärung: Unveränderliches Objekt, das einen fachlichen Wert ausdrückt.

Beispiel im Lernprojekt: ClaimId, Money, RiskScore.

Warum wichtig: Validierung und Bedeutung liegen am richtigen Ort.

Aggregate

Erklärung: Konsistenzgrenze im Domain Model.

Beispiel im Lernprojekt: Claim mit Entscheidungen und Dokumentverweisen.

Warum wichtig: Schützt Invarianten bei Änderungen.

Repository Pattern

Erklärung: Abstraktion zum Laden und Speichern fachlicher Aggregate.

Beispiel im Lernprojekt: ClaimRepository.

Warum wichtig: Persistenzdetails bleiben außerhalb der Domäne.

Application Service

Erklärung: Use-Case-Schicht, die Transaktion, Ports und Domänenregeln koordiniert.

Beispiel im Lernprojekt: ProcessClaimDecisionHandler.

Warum wichtig: Ersetzt EJB-Monsterlogik.

Command Object

Erklärung: Objekt, das Eingaben eines Use Cases bündelt.

Beispiel im Lernprojekt: ProcessClaimDecisionCommand.

Warum wichtig: Ersetzt lange Parameterlisten.

Result Object

Erklärung: Objekt, das fachliches Ergebnis und Folgeinformationen strukturiert zurückgibt.

Beispiel im Lernprojekt: ProcessClaimDecisionResult.

Warum wichtig: Ersetzt technische Returncodes und Exceptions.

Policy Object

Erklärung: Kapselt eine fachliche Regel.

Beispiel im Lernprojekt: AuthorizationPolicy, NotificationTemplatePolicy.

Warum wichtig: Regeln sind einzeln testbar.

State Pattern

Erklärung: Statusabhängiges Verhalten wird aus if/else-Ketten in explizite Status-/Transitionslogik verschoben.

Beispiel im Lernprojekt: ClaimTransitionPolicy.

Warum wichtig: Statusübergänge werden verständlich.

Specification Pattern

Erklärung: Kombinierbare fachliche Prüfregeln.

Beispiel im Lernprojekt: CoverageSpecification.

Warum wichtig: Komplexe Regeln werden modular.

Facade Pattern

Erklärung: Vereinfachte Schnittstelle vor komplexem Subsystem.

Beispiel im Lernprojekt: LegacyClaimFacade als Übergangspunkt.

Warum wichtig: Ermöglicht erste sichere Schnittlinie.

Workflow Orchestrator

Erklärung: Komponente, die fachliche Prozessschritte koordiniert, ohne technische Details zu besitzen.

Beispiel im Lernprojekt: ClaimDecisionWorkflowOrchestrator.

Warum wichtig: Workflow wird nachvollziehbar und testbar.

Approval Chain

Erklärung: Genehmigungskette für Entscheidungen, Zahlungen oder Eskalationen.

Beispiel im Lernprojekt: Teamlead Approval vor Payment Release.

Warum wichtig: Ersetzt verstreute Rollenprüfungen.

Shared Kernel

Erklärung: Gemeinsamer Kern stabiler Typen, Fehler und Basisabstraktionen.

Beispiel im Lernprojekt: shared-kernel Modul.

Warum wichtig: Verhindert Duplikate und unklare Basistypen.

Bounded Context

Erklärung: Fachliche Grenze mit eigener Sprache und eigenen Modellen.

Beispiel im Lernprojekt: Claim, Payment, Document, Audit.

Warum wichtig: Vermeidet ein übergroßes gemeinsames Modell.

Zusatzsysteme und Integrationen

Partner Portal

Erklärung: Externes Portal für Partner wie Werkstätten, Makler oder Schadensmelder.

Beispiel im Lernprojekt: legacy-claims-partner-portal.

Warum wichtig: Kann über REST/BFF stranguliert werden.

Internal Backoffice

Erklärung: Interne Anwendung für Sachbearbeiter, Teamleiter und Spezialisten.

Beispiel im Lernprojekt: legacy-internal-claims-backoffice.

Warum wichtig: Backoffice API entkoppelt UI und Use Cases.

Document Management Adapter

Erklärung: Adapter zur Ablage, Prüfung, Versionierung und Archivierung von Dokumenten.

Beispiel im Lernprojekt: legacy-document-management-adapter.

Warum wichtig: File Share kann später ersetzt werden.

Document Storage Port

Erklärung: Schnittstelle zum Speichern und Laden von Dokumenten.

Beispiel im Lernprojekt: DocumentStoragePort.

Warum wichtig: Ablage ist austauschbar.

Document Verification Port

Erklärung: Schnittstelle zur fachlichen Dokumentenprüfung.

Beispiel im Lernprojekt: DocumentVerificationPort.

Warum wichtig: SOAP-Prüfdienst wird entkoppelt.

Fraud Risk Gateway

Erklärung: Integration zur Betrugsrisikoprüfung.

Beispiel im Lernprojekt: FraudRiskSoapClient.

Warum wichtig: RiskScore wird fachlich modelliert.

Fraud Risk Port

Erklärung: Fachlicher Port zur Risikoabfrage.

Beispiel im Lernprojekt: FraudRiskPort.

Warum wichtig: Timeouts und XML bleiben im Adapter.

Policy Coverage System

Erklärung: System zur Prüfung der Versicherungsdeckung.

Beispiel im Lernprojekt: PolicyCoverageSoapClient.

Warum wichtig: CoverageDecision ersetzt technische Antwortformate.

Coverage Port

Erklärung: Fachliche Schnittstelle zur Deckungsprüfung.

Beispiel im Lernprojekt: CoveragePort.

Warum wichtig: Regeln werden test- und austauschbar.

Payment Release System

Erklärung: Altsystem zur Freigabe und Auslösung von Zahlungen.

Beispiel im Lernprojekt: PaymentReleaseSoapClient.

Warum wichtig: PaymentReleasePort plus Idempotency schützt vor Doppelzahlung.

Payment Release Port

Erklärung: Schnittstelle zum Zahlungssystem.

Beispiel im Lernprojekt: PaymentReleasePort.

Warum wichtig: Kompensation und Fehlerbehandlung werden kontrolliert.

Notification System

Erklärung: System für E-Mail, SMS, interne Nachrichten und Templates.

Beispiel im Lernprojekt: LegacyNotificationSenderBean.

Warum wichtig: NotificationPort entkoppelt Benachrichtigung.

Notification Port

Erklärung: Fachlicher Port zum Versand von Benachrichtigungen.

Beispiel im Lernprojekt: NotificationPort.

Warum wichtig: Outbox und Retry werden möglich.

SLA & Escalation Scheduler

Erklärung: Scheduler für Fristen, Eskalationen und SLA-Verletzungen.

Beispiel im Lernprojekt: SlaEscalationTimerBean.

Warum wichtig: OpenShift CronJob und EscalationPolicy ersetzen TimerBean.

Audit & Compliance Reporter

Erklärung: System für Nachvollziehbarkeit, Reports und Compliance-Auswertungen.

Beispiel im Lernprojekt: AuditReportStoredProcedureDao.

Warum wichtig: AuditLogPort und Reporting Read Model trennen Verantwortung.

Search Indexer

Erklärung: Suchkomponente für Claims, Dokumente, Partnerfälle oder Kunden.

Beispiel im Lernprojekt: LegacyClaimSearchDao.

Warum wichtig: ClaimSearchPort trennt Suchdomäne.

Customer Master Data System

Erklärung: Stammdatensystem für Kundendaten, Adressen und Segmente.

Beispiel im Lernprojekt: CustomerMasterDataSoapClient.

Warum wichtig: CustomerSnapshot stabilisiert Daten.

Identity & Role Management

Erklärung: System für Benutzer, Gruppen, Rollen und Berechtigungen.

Beispiel im Lernprojekt: JaasRoleChecker.

Warum wichtig: SecurityContextPort + AuthorizationPolicy trennen Security vom Fachcode.

Messaging, Transaktionen und Resilienz

Outbox Pattern

Erklärung: Speichert fachliche Änderung und zu sendendes Event in derselben Datenbanktransaktion.

Beispiel im Lernprojekt: OutboxEvent nach Claim Decision.

Warum wichtig: Verhindert verlorene Events.

Idempotency

Erklärung: Operation kann mehrfach aufgerufen werden, ohne doppelte fachliche Wirkung.

Beispiel im Lernprojekt: Payment Idempotency Key.

Warum wichtig: Schützt bei Retry und Netzwerkfehlern.

Retry Policy

Erklärung: Regel für Wiederholungsversuche technischer Aufrufe.

Beispiel im Lernprojekt: Notification Retry.

Warum wichtig: Fehlerbehandlung wird explizit.

Circuit Breaker

Erklärung: Schaltet instabile externe Systeme vorübergehend ab.

Beispiel im Lernprojekt: FraudRiskService Timeout.

Warum wichtig: Verhindert Kaskadenausfälle.

Timeout Policy

Erklärung: Begrenzt Wartezeit auf externe Systeme.

Beispiel im Lernprojekt: SOAP-Aufruf zum Coverage System.

Warum wichtig: Threads und Transaktionen werden geschützt.

Dead Letter Queue

Erklärung: Ablage für Nachrichten, die nicht erfolgreich verarbeitet werden konnten.

Beispiel im Lernprojekt: Notification DLQ.

Warum wichtig: Fehler bleiben sichtbar und analysierbar.

Compensation

Erklärung: Ausgleichsaktion nach teilweisem Erfolg.

Beispiel im Lernprojekt: Payment stornieren, wenn Folgeprozess fehlschlägt.

Warum wichtig: Ersetzt unrealistische globale Transaktionen.

Exactly Once Illusion

Erklärung: Irrtum, dass verteilte Systeme immer exakt einmal ausführen können.

Beispiel im Lernprojekt: Payment und Notification.

Warum wichtig: Idempotency und Outbox sind realistischer.

At-Least-Once Delivery

Erklärung: Nachricht kann mindestens einmal, ggf. mehrfach ankommen.

Beispiel im Lernprojekt: Outbox Publisher.

Warum wichtig: Consumer müssen idempotent sein.

Transactional Boundary

Erklärung: Bewusste Grenze einer Transaktion.

Beispiel im Lernprojekt: Claim speichern + Outbox schreiben.

Warum wichtig: Externe SOAP-Aufrufe werden nicht blind in JTA gepackt.

Eventual Consistency

Erklärung: Systeme werden nach kurzer Zeit konsistent, nicht zwingend sofort.

Beispiel im Lernprojekt: Search Read Model nach Claim Update.

Warum wichtig: Erlaubt entkoppelte Verarbeitung.

Correlation ID

Erklärung: Kennung zur Verfolgung eines Vorgangs über Systemgrenzen.

Beispiel im Lernprojekt: Claim Decision Request.

Warum wichtig: Verbessert Debugging und Audit.

Lease Lock

Erklärung: Sperrmechanismus, damit Batchjobs in mehreren Pods nicht doppelt laufen.

Beispiel im Lernprojekt: EscalationBatchJob.

Warum wichtig: Kubernetes-Skalierung bleibt kontrolliert.

OpenShift, Container und Betrieb

OpenShift

Erklärung: Enterprise-Kubernetes-Plattform von Red Hat für Build, Deployment, Security und Betrieb.

Beispiel im Lernprojekt: Zielplattform.

Warum wichtig: Löst manuelle WebSphere-Deployments ab.

Containerfile

Erklärung: Anweisung zum Bauen eines Container-Images.

Beispiel im Lernprojekt: Containerfile.openliberty.

Warum wichtig: Runtime wird reproduzierbar.

Open Liberty

Erklärung: Leichtgewichtige Java-Enterprise-Runtime für containerisierte Workloads.

Beispiel im Lernprojekt: Open Liberty Zielruntime.

Warum wichtig: Näher an Jakarta/MicroProfile und Containerbetrieb.

Spring Boot

Erklärung: Java-Anwendungsframework mit eingebettetem Runtime-Modell.

Beispiel im Lernprojekt: Alternative Zielruntime.

Warum wichtig: Erlaubt moderne REST- und Betriebsendpunkte.

ConfigMap

Erklärung: Kubernetes/OpenShift-Objekt für nicht-geheime Konfiguration.

Beispiel im Lernprojekt: Feature Flags, URLs, Timeouts.

Warum wichtig: Ersetzt harte Konfiguration im Code.

Secret

Erklärung: Kubernetes/OpenShift-Objekt für sensible Konfiguration.

Beispiel im Lernprojekt: DB-Passwort, SOAP Credentials.

Warum wichtig: Keine Passwörter im Code.

Route

Erklärung: OpenShift-Ressource zur externen Erreichbarkeit eines Service.

Beispiel im Lernprojekt: claim-rest-api Route.

Warum wichtig: Kontrollierter Zugriff von außen.

Service

Erklärung: Kubernetes-Abstraktion für stabile Netzwerkadresse zu Pods.

Beispiel im Lernprojekt: claim-service.

Warum wichtig: Pods können ersetzt werden.

Deployment

Erklärung: Kubernetes-Ressource zur Verwaltung von Pods und Rollouts.

Beispiel im Lernprojekt: claim-service Deployment.

Warum wichtig: Reproduzierbare Releases.

CronJob

Erklärung: Kubernetes/OpenShift-Ressource für geplante Jobs.

Beispiel im Lernprojekt: Escalation CronJob.

Warum wichtig: Ersetzt EJB Timer.

Liveness Probe

Erklärung: Prüft, ob ein Container noch lebt.

Beispiel im Lernprojekt: Health Endpoint.

Warum wichtig: Defekte Pods werden neu gestartet.

Readiness Probe

Erklärung: Prüft, ob ein Pod bereit ist, Traffic anzunehmen.

Beispiel im Lernprojekt: DB- und Dependency Check.

Warum wichtig: Verhindert Traffic auf nicht bereite Pods.

Startup Probe

Erklärung: Prüft den Startvorgang langsamer Anwendungen.

Beispiel im Lernprojekt: Open Liberty Start.

Warum wichtig: Verhindert zu frühe Neustarts.

HPA

Erklärung: Horizontal Pod Autoscaler.

Beispiel im Lernprojekt: Skalierung nach CPU oder Metriken.

Warum wichtig: Betrieb reagiert auf Last.

PDB

Erklärung: PodDisruptionBudget.

Beispiel im Lernprojekt: Mindestverfügbarkeit bei Wartung.

Warum wichtig: Schützt gegen zu viele gleichzeitige Ausfälle.

NetworkPolicy

Erklärung: Kubernetes-Regel für Netzwerkzugriffe zwischen Pods.

Beispiel im Lernprojekt: Claim Service darf nur erlaubte Ziele erreichen.

Warum wichtig: Reduziert Angriffsfläche.

Observability

Erklärung: Sammelbegriff für Logs, Metriken, Traces und Health.

Beispiel im Lernprojekt: Structured Logs, Metrics, Tracing.

Warum wichtig: Betrieb wird nachvollziehbar.

Structured Logging

Erklärung: Logs mit Feldern statt nur Textzeilen.

Beispiel im Lernprojekt: claimId, correlationId, decision.

Warum wichtig: Bessere Suche und Analyse.

Metrics

Erklärung: Messwerte über Laufzeit und Verhalten.

Beispiel im Lernprojekt: Decision Duration, Error Count.

Warum wichtig: Monitoring und Alerting werden möglich.

Tracing

Erklärung: Verfolgung eines Requests über Service- und Systemgrenzen.

Beispiel im Lernprojekt: SOAP -> Handler -> Payment -> Outbox.

Warum wichtig: Hilft bei Fehleranalyse.

Dokumentation, Change Management und Migration

Before/After Mapping

Erklärung: Zuordnung von Legacy-Dateien zu modernen Modulen und Dateien.

Beispiel im Lernprojekt: LegacyClaimFacadeBean -> ProcessClaimDecisionHandler.

Warum wichtig: Migrationsnutzen wird nachvollziehbar.

Changelog V2

Erklärung: Strukturierte Änderungsdokumentation mit Kategorie, Grund, Risiko, Tests und Migrationsnutzen.

Beispiel im Lernprojekt: CHANGELOG_V2.md.

Warum wichtig: Architekturänderungen bleiben erklärbar.

Change Matrix

Erklärung: Tabellarische Übersicht über Änderungen und betroffene Module.

Beispiel im Lernprojekt: CHANGELOG_V2_MATRIX.csv.

Warum wichtig: Hilft bei Review und Audit.

Migration Decision

Erklärung: Dokumentierte Entscheidung mit Kontext, Option, Begründung und Risiko.

Beispiel im Lernprojekt: MIGRATION_DECISIONS.md.

Warum wichtig: Verhindert verlorenes Architekturwissen.

Risk Register

Erklärung: Liste technischer und fachlicher Risiken mit Gegenmaßnahmen.

Beispiel im Lernprojekt: RISKS.md.

Warum wichtig: Macht Migration planbarer.

Rollback Plan

Erklärung: Plan zur Rückkehr auf einen sicheren Zustand.

Beispiel im Lernprojekt: Cutover mit Rückfalloption.

Warum wichtig: Reduziert Produktionsrisiko.

Cutover

Erklärung: Geplanter Umschaltzeitpunkt von Legacy auf Zielsystem oder Teilfunktion.

Beispiel im Lernprojekt: SOAP Boundary parallel betreiben.

Warum wichtig: Migration wird kontrolliert.

Strangler Pattern

Erklärung: Neue Funktionalität wird schrittweise um das Legacy-System herum aufgebaut.

Beispiel im Lernprojekt: Partner Portal wird über REST/BFF entkoppelt.

Warum wichtig: Kein Big-Bang-Austausch nötig.

Parallel Run

Erklärung: Legacy und Zielsystem laufen zeitweise parallel zur Verifikation.

Beispiel im Lernprojekt: Legacy-vs-Modular Vergleich.

Warum wichtig: Erhöht Sicherheit vor Abschaltung.

Smoke Test

Erklärung: Schneller End-to-End-Test der wichtigsten Funktion nach Deployment.

Beispiel im Lernprojekt: REST Smoke IT.

Warum wichtig: Findet grobe Deploy-Fehler früh.

Migration Test

Erklärung: Test zur Absicherung der Funktion auf Zielplattform.

Beispiel im Lernprojekt: OpenShiftDeploymentSmokeIT.

Warum wichtig: Sichert Runtime- und Konfigurationswechsel.

Artifact Manifest

Erklärung: Maschinenlesbare Liste erzeugter Dateien und Pakete.

Beispiel im Lernprojekt: artifact_manifest.json.

Warum wichtig: Erleichtert Prüfung des Gesamtpakets.

Erklärung: Prüfung interner Links und Anker.

Beispiel im Lernprojekt: link_check.md.

Warum wichtig: Verhindert kaputte Navigation.

PDF Check

Erklärung: Prüfung von PDF-Seiten, Inhaltsverzeichnis, Bookmarks und Lesbarkeit.

Beispiel im Lernprojekt: pdf_check.md.

Warum wichtig: Sichert brauchbare Ausgabe.

Fachbegriffe im Claims-Kontext

Claim

Erklärung: Schadensfall oder Leistungsfall, der gemeldet, geprüft, entschieden und ggf. bezahlt wird.

Beispiel im Lernprojekt: Claim Aggregate.

Warum wichtig: Zentrales fachliches Objekt.

Claim Status

Erklärung: Aktueller Zustand eines Schadensfalls.

Beispiel im Lernprojekt: CREATED, DOCUMENTS_PENDING, APPROVED.

Warum wichtig: Statuslogik wird über Policies gesteuert.

Claim Decision

Erklärung: Fachliche Entscheidung über den weiteren Verlauf eines Claims.

Beispiel im Lernprojekt: Approve, Reject, Escalate.

Warum wichtig: Use Case wird explizit modelliert.

Coverage

Erklärung: Deckungsprüfung, ob Police und Schaden zusammenpassen.

Beispiel im Lernprojekt: CoverageDecision.

Warum wichtig: Externe Tariflogik wird entkoppelt.

Deductible

Erklärung: Selbstbehalt, den der Kunde selbst tragen muss.

Beispiel im Lernprojekt: Coverage Calculation.

Warum wichtig: Fachwert statt bloßer Zahl.

Fraud Risk

Erklärung: Einschätzung eines möglichen Betrugsrisikos.

Beispiel im Lernprojekt: RiskScore.

Warum wichtig: Technische Risikoantwort wird fachlich nutzbar.

Document Verification

Erklärung: Prüfung von eingereichten Dokumenten auf Vollständigkeit und Gültigkeit.

Beispiel im Lernprojekt: verifyClaimDocument.

Warum wichtig: Eigene Port-Grenze statt Fachlogik im EJB.

Payment Approval

Erklärung: Fachliche Freigabe einer Auszahlung.

Beispiel im Lernprojekt: approveClaimPayment.

Warum wichtig: Approval Chain und Payment Port trennen Regeln und Technik.

Escalation

Erklärung: Hochstufung eines Falls bei Fristverletzung, Risiko oder Sonderentscheidung.

Beispiel im Lernprojekt: escalateClaim.

Warum wichtig: EscalationPolicy ersetzt TimerBean-Logik.

SLA

Erklärung: Service Level Agreement, also vereinbarte Bearbeitungsfrist oder Qualitätsvorgabe.

Beispiel im Lernprojekt: SLA & Escalation Scheduler.

Warum wichtig: Fristen werden testbar berechnet.

Audit Trail

Erklärung: Chronologische, fachlich verständliche Historie eines Falls.

Beispiel im Lernprojekt: getClaimAuditTrail.

Warum wichtig: Compliance wird nachvollziehbar.

Customer Snapshot

Erklärung: Stabile Momentaufnahme relevanter Kundendaten.

Beispiel im Lernprojekt: CustomerSnapshot Value Object.

Warum wichtig: Schützt vor inkonsistenten Stammdaten.

Partner Identity

Erklärung: Identität eines externen Partners mit Rollen und Zugriffen.

Beispiel im Lernprojekt: Partner Identity Service.

Warum wichtig: Partnerzugriffe werden kontrolliert.