Legacy Claims Portal 🏠 Home 📘 Lernpfad đŸ§Ș Workbook 📚 Handbuch đŸ§© Patterns đŸ§Ÿ Lizenzen 🔎 Archiv

Legacy Claims & Customer Support Enterprise System

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

Navigation
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

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(...)

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

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

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

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

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

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

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.

⌂ Cockpit