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

Legacy Claims & Customer Support Enterprise System

Legacy Claims & Customer Support Enterprise System

Konsolidierte, ĂŒbersichtlich gegliederte Fassung aus dem Rohmaterial. Hauptkapitel sind bewusst wenige, große Lerneinheiten. Unterpunkte wie 3.1 sind keine eigenen Kapitel mehr, sondern normale Abschnitte innerhalb des Hauptkapitels. Reine Planungsabschnitte wurden entfernt, damit das Lernbuch wie ein fertiges Handbuch wirkt.

1. EinfĂŒhrung und Lesehinweis

Kapitelziel

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

ProjektĂŒberblick

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

Lesehinweis

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

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

Fachliche Visualisierungshinweise

Die HTML-Ausgabe nutzt kompakte fachliche SVG-Skizzen dort, wo sie das VerstÀndnis verbessern: Architekturgrenzen, Legacy-Kopplung, Refactoring-Fluss, Mapping, Risiken, Glossar und Codebeispiel-Leselogik. Diese Grafiken ersetzen keine Inhalte, sondern dienen als schnelle Orientierung vor detailreichen Abschnitten.

Konsolidierte Struktur

2. Zielbild und Gesamtstruktur

Kapitelziel

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

Zielbild, Systemlandschaft und Gesamtstruktur

Rundennummer: 01 Datum: 2026-07-05 Arbeitsmodus: Große Runde 1 Projektname: Legacy Claims & Customer Support Enterprise System Ziel dieser Runde: Zielbild, Systemlandschaft, Gesamtstruktur, Modulgrenzen, Artefaktstruktur und Migrations-/Refactoring-Rahmen definieren.

Ergebnis dieser Runde

In dieser Runde wurde das Projekt als großes Enterprise-Lernprojekt strukturiert. Der Fokus liegt noch nicht auf vollstĂ€ndigem Legacy-Code und noch nicht auf der Monster Method processClaimDecision(...). Diese Themen kommen in Runde 2 und Runde 3.

Runde 1 liefert die fachliche und technische Landkarte:

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

  1. Legacy absichern

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

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

  1. 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:

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:

Jedes Zusatzsystem bekommt spÀter eigene Codebeispiele, Adapter, Tests, Before/After-Mapping und EintrÀge im Changelog.

Rohmaterial bleibt erhalten

Nach jeder Runde entsteht eine neue Roh-MD-Datei. Diese Rohdateien werden nicht ĂŒberschrieben. SpĂ€ter entsteht zusĂ€tzlich eine konsolidierte schöne Fassung.

Gesamt-Systemlandschaft

Fachlicher Mittelpunkt

Im Zentrum steht der Schadensfall:

Claim
 ├─ Kunde
 ├─ Police / Vertrag / Deckung
 ├─ Dokumente
 ├─ Fraud-Risiko
 ├─ Status / Workflow
 ├─ Entscheidung
 ├─ Zahlung
 ├─ Audit Trail
 ├─ SLA / Eskalation
 └─ Kommunikation / Benachrichtigung

Der Schadensfall ist nicht nur ein Datensatz. Er ist ein fachlicher Prozess. Der Prozess umfasst Erfassung, DokumentenprĂŒfung, DeckungsprĂŒfung, Betrugsrisikobewertung, Entscheidung, Genehmigung, Zahlung, Benachrichtigung und Auditierbarkeit.

Legacy-Landschaft als Textdiagramm

                               +----------------------------------+

                               | Legacy Identity & Role Management |
                               | LDAP / JAAS / WebSphere Roles     |
                               +-------------------+--------------+

                                                   |
                                                   v
+-----------------------------+      SOAP      +-----------------------------+

| Legacy Claims Partner Portal|  ----------->  | Legacy Claims Facade EJB    |
| JSP / Servlet / Upload      |                | WebSphere / EAR / EJB-JAR   |
+-----------------------------+                +-------------+---------------+

                                                               |
+-----------------------------+                                |

| Legacy Internal Backoffice  |  JSP/EJB/Stored Procedures      |
| JSP / Servlet / Oracle      |  ------------------------------+
+-----------------------------+                                |
                                                               v
                                         +--------------------------------------+

                                         | Monster Method                       |
                                         | processClaimDecision(...)             |
                                         | if/else, SOAP, JMS, JDBC, Files, JAAS |
                                         +---+-----------+-----------+----------+

                                             |           |           |
                       +---------------------+           |           +----------------------+
                       v                                 v                                  v
        +-----------------------------+   +------------------------------+   +-----------------------------+

        | Policy Coverage System      |   | Fraud Risk Gateway           |   | Document Management Adapter  |
        | SOAP / Oracle / Rules       |   | SOAP / XML / Timeout Issues  |   | File Share / SOAP / XML      |
        +-----------------------------+   +------------------------------+   +-----------------------------+

                       |
                       v
        +-----------------------------+   +------------------------------+   +-----------------------------+

        | Payment Release System      |   | Notification System          |   | Audit & Compliance Reporter |
        | SOAP / JMS / JTA            |   | JMS / SOAP / Email / SMS     |   | Stored Procedures / CSV      |
        +-----------------------------+   +------------------------------+   +-----------------------------+

        +-----------------------------+   +------------------------------+   +-----------------------------+

        | SLA & Escalation Scheduler  |   | Legacy Search Indexer        |   | Customer Master Data System |
        | EJB Timer / Scheduler       |   | Oracle LIKE / Batch Indexer  |   | SOAP / Oracle / Customer ID |
        +-----------------------------+   +------------------------------+   +-----------------------------+

Ziel-Landschaft als Textdiagramm

+----------------------------------------------------------------------------------+

|                                  OpenShift                                        |
|  +------------------+       +----------------------+       +------------------+  |
|  | claim-rest-api   | ----> | claim-application    | ----> | claim-domain     |  |
|  | REST / OpenAPI   |       | Use Cases / Commands |       | Entities/Policy  |  |
|  +------------------+       +----------+-----------+       +------------------+  |
|                                      |                                           |
|              +-----------------------+------------------------+                  |
|              |                       |                        |                  |
|              v                       v                        v                  |
|  +---------------------+  +----------------------+  +-------------------------+ |
|  | claim-document      |  | claim-workflow       |  | claim-payment           | |
|  | Ports + Adapters    |  | Orchestrator/State   |  | Port/Adapter/Outbox     | |
|  +---------------------+  +----------------------+  +-------------------------+ |
|              |                       |                        |                  |
|              v                       v                        v                  |
|  +---------------------+  +----------------------+  +-------------------------+ |
|  | claim-audit         |  | claim-notification   |  | claim-search            | |
|  | Audit Trail         |  | Events/Templates     |  | Read Model/Search Port  | |
|  +---------------------+  +----------------------+  +-------------------------+ |
|  +------------------+ +---------------------+ +-------------------------------+ |
|  | claim-batch      | | claim-soap-api       | | claim-infrastructure          | |
|  | CronJob Worker   | | SOAP Compatibility   | | SOAP/JMS/DB/File/Observability| |
|  +------------------+ +---------------------+ +-------------------------------+ |
+----------------------------------------------------------------------------------+

DomÀnenlandkarte

Core Domain

Die zentrale FachdomÀne ist das Claims Management:

Supporting Domains

Supporting Domains unterstĂŒtzen die Kernentscheidung:

Supporting DomainAufgabeTypische Legacy-KopplungModerne Grenze
Document ManagementDokumente speichern, prĂŒfen, archivierenFile Share, SOAP, XML-MetadatenDocumentStoragePort, DocumentVerificationPort
Fraud RiskRisiko prĂŒfen, Score abrufenSOAP-Client direkt in EJBFraudRiskPort
Policy CoverageDeckung und AusschlĂŒsse prĂŒfenSOAP + Stored ProceduresCoveragePort, CoveragePolicy
Payment ReleaseZahlung vorbereiten/freigebenSOAP/JMS/JTA direkt im WorkflowPaymentReleasePort, Outbox
NotificationKunden/Partner/Agenten informierenJMS/SOAP/Template-DateienNotificationPort
Audit/ComplianceEntscheidungen nachvollziehbar machenStored Procedures, technische LogsAuditLogPort, Audit Events
SearchClaims/Kunden/Dokumente suchenOracle LIKE, Batch IndexerClaimSearchPort
SLA/EscalationFristen, Verletzungen, EskalationenTimerBean, Scheduler, Stored ProcEscalationPolicy, claim-batch
Customer Master DataKundendaten abrufenSOAP, alte KundennummernCustomerMasterDataPort
Identity/RoleBenutzer/Rollen prĂŒfenLDAP/JAAS/WebSphere RolesSecurityContextPort, AuthorizationPolicy

IntegrationsdomÀnen

Integrationen sind im Zielsystem keine zufÀlligen technischen Aufrufe mehr, sondern explizite Grenzen:

Application Use Case
   -> Port Interface
      -> Adapter
         -> Legacy SOAP / JMS / DB / File Share / OpenShift Service

Zielmodule

Die Zielmodule bilden fachliche und technische Grenzen. Der Package-Stamm lautet immer:

com.seb4u.demo.claims

ModulĂŒbersicht

ModulZweckEnthÀlt
shared-kernelGemeinsame BasistypenIDs, Money, Result, Error Codes, Clock, CorrelationId
claim-domainReine FachlogikClaim, ClaimStatus, Policies, State, Value Objects
claim-applicationUse CasesCommand Handler, Application Services, Ports
claim-infrastructuretechnische AdapterSOAP Clients, JMS, JDBC, JPA, File Share, Observability
claim-soap-apiLegacy-kompatible SOAP BoundaryWSDL-kompatible Endpoints, DTO Mapping
claim-rest-apimoderne APIREST Controller, DTOs, OpenAPI-Anbindung
claim-batchBatch/SchedulerEscalation Jobs, SLA Worker, OpenShift CronJob
claim-documentDokumentengrenzeDocument Ports, Document Policies, Adapters
claim-workflowWorkflowOrchestrator, State Machine, Approval Chain
claim-searchSucheSearch Port, Search Read Model, Adapter
claim-notificationBenachrichtigungNotification Port, Templates, Events
claim-auditAudit TrailAudit Events, AuditLogPort, Reporting Read Model
claim-paymentZahlungPaymentReleasePort, Idempotency, Compensation
claim-openapiAPI-SpezifikationOpenAPI YAML/JSON, generated docs
migration-testsMigrationssicherungSOAP/REST Compatibility, Contract, Smoke Tests
legacy-websphere-earLegacy AusgangspunktEAR/WAR/EJB Struktur
legacy-claims-partner-portalexternes PortalJSP/Servlet/SOAP Client
legacy-internal-claims-backofficeinternes BackofficeJSP/EJB/Stored Proc
legacy-document-management-adapterDMS LegacyFile Share/SOAP/XML
legacy-fraud-risk-gatewayFraud LegacySOAP/XML
legacy-policy-coverage-systemCoverage LegacySOAP/Oracle/Stored Proc
legacy-payment-release-systemPayment LegacySOAP/JMS/JTA
legacy-notification-systemNotification LegacyJMS/SOAP/Email/SMS
legacy-sla-escalation-schedulerSLA LegacyTimerBean/Scheduler
legacy-audit-compliance-reporterReporting LegacyStored Procedures/CSV
legacy-search-indexerSearch LegacyOracle LIKE/Batch
legacy-customer-master-data-systemCustomer LegacySOAP/Oracle
legacy-identity-role-managementIdentity LegacyLDAP/JAAS/Roles

Vorgeschlagene Gesamtstruktur des Beispielprojekts

legacy-claims-customer-support-enterprise/
  README.md
  pom.xml

  before_refactoring_legacy/
    legacy-websphere-ear/
    legacy-claims-partner-portal/
    legacy-internal-claims-backoffice/
    legacy-document-management-adapter/
    legacy-fraud-risk-gateway/
    legacy-policy-coverage-system/
    legacy-payment-release-system/
    legacy-notification-system/
    legacy-sla-escalation-scheduler/
    legacy-audit-compliance-reporter/
    legacy-search-indexer/
    legacy-customer-master-data-system/
    legacy-identity-role-management/

  refactoring_steps/
    step_01_inventory_characterization/
    step_02_system_landscape/
    step_03_monster_method_understanding/
    step_04_extract_facade_and_command/
    step_05_extract_state_policy/
    step_06_extract_coverage_fraud_document_payment_ports/
    step_07_extract_customer_identity_ports/
    step_08_workflow_orchestrator/
    step_09_audit_trail_abstraction/
    step_10_search_notification_reporting_split/
    step_11_outbox_idempotency/
    step_12_batch_sla_escalation/
    step_13_portal_backoffice_decoupling/
    step_14_openshift_preparation/

  after_refactoring_modular/
    shared-kernel/
    claim-domain/
    claim-application/
    claim-infrastructure/
    claim-soap-api/
    claim-rest-api/
    claim-batch/
    claim-document/
    claim-workflow/
    claim-search/
    claim-notification/
    claim-audit/
    claim-payment/
    claim-openapi/
    migration-tests/

  migration_to_openshift/
    docker/
    openshift/
      base/
      overlays/
        dev/
        test/
        prod/
    config/
    secrets-examples/
    runbooks/

  tests/
    characterization-tests/
    golden-master-tests/
    soap-contract-tests/
    adapter-tests/
    archunit-tests/
    migration-tests/
    smoke-tests/

  docs/
    architecture/
    decisions/
    diagrams/
    migration/
    operations/
    risks/

Artefaktstruktur

Das finale Gesamtpaket soll spÀter so aufgebaut werden:

00_rohmaterial_wachsend/
  projekt_rohmaterial_wachsend.zip
  rohmaterial/
    runde_01_roh_1zu1.md
    runde_02_roh_1zu1.md
    runde_03_roh_1zu1.md
    ...
  index/
    rohmaterial_index.md
  manifest/
    rohmaterial_manifest.json

01_lernbuch/
  masterbook.md
  pc.html
  mobile.html
  pc.pdf
  mobile.pdf

02_beispielprojekt/
  legacy-claims-customer-support-enterprise.zip

03_refactoring_changes/
  refactoring_phases.zip
  essential_changes.zip
  changelog_v2.zip
  before_after_mapping.zip

04_pruefberichte/
  manifest.json
  link_check.md
  pdf_check.md
  zip_check.md
  structure_check.md
  svg_check.md
  artifact_manifest.json

05_gesamtpaket/
  legacy_claims_customer_support_gesamtpaket.zip

Refactoring-Phasen-Paket

Die Refactoring-Phasen sind spÀter nicht nur Dokumentation, sondern didaktische ZwischenstÀnde. Dadurch kann man sehen:

01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/

Refactoring-Phasen

PhaseThemaZiel
01Inventory und CharacterizationVerstehen, was wirklich existiert
02Systemlandschaft und Zusatzsysteme erfassenIntegrationen und AbhÀngigkeiten sichtbar machen
03Monster Method verstehenprocessClaimDecision(...) analysieren
04Facade und Command extrahierenUse Case von EJB lösen
05Statuslogik in State/Policy ĂŒberfĂŒhrenif/else-Ketten reduzieren
06Coverage, Fraud, Document, Payment als Portsexterne Systeme entkoppeln
07Customer und Identity als PortsStammdaten und Security entkoppeln
08Workflow OrchestratorProzesslogik sichtbar machen
09Audit Trail abstrahierenfachliche Nachvollziehbarkeit sichern
10Search, Notification, Reporting trennenNebenlogik aus Use Case lösen
11Outbox und Idempotencyrobuste Integration vorbereiten
12Batch und SLA trennenTimerBean ablösen
13Portal und Backoffice entkoppelnUI schrittweise modernisieren
14OpenShift vorbereitenRuntime-Grenzen und Konfiguration klÀren

Essential-Changes-Paket

Das Essential-Changes-Paket soll spÀter die wichtigsten ArchitekturÀnderungen fachlich nachvollziehbar machen.

01_change_catalog/
02_before_legacy_examples/
03_after_modular_examples/
04_tests_and_safety_net/
05_migration_changes/
06_original_project_zip/

Change Catalog Struktur

Jede Änderung bekommt spĂ€ter:

Beispiel:

FeldBeispiel
ÄnderungMonster Method zerlegt
VorherLegacyClaimFacadeBean.processClaimDecision(...)
NachherProcessClaimDecisionHandler + Policies + Ports
PatternCommand Handler, Application Service, Ports & Adapters
Ordnerbefore_refactoring_legacy, after_refactoring_modular/claim-application
TestGolden Master + Handler Test
NutzenFachlogik wird ohne WebSphere testbar

Changelog V2 Zielstruktur

SpÀter werden erzeugt:

CHANGELOG_V2.md
CHANGELOG_V2_KURZFASSUNG.md
CHANGELOG_V2_MATRIX.csv
CHANGELOG_V2.json

Changelog-Felder

FeldBedeutung
IDeindeutige Änderungs-ID
KategorieRefactoring, Migration, Tests, Security, Integration
Änderungkurze Beschreibung
VorherLegacy-Zustand
Nachhermoderner Zustand
betroffene ModuleModule und Ordner
Pattern / Architekturkonzeptz. B. State, Policy, Port, Adapter
Warum geÀndert?fachlicher/technischer Grund
MigrationsnutzenNutzen fĂŒr OpenShift/Modernisierung
Tests / Sicherheitsnetzkonkrete Tests
Risiko / Achtungverbleibende Risiken

Before/After-Mapping Zielstruktur

SpÀter werden erzeugt:

BEFORE_AFTER_MAPPING.md
BEFORE_AFTER_MAPPING.csv
BEFORE_AFTER_MAPPING.json
BEFORE_AFTER_MAPPING_KURZFASSUNG.md

Mapping-Grundidee

Legacy-Datei / Legacy-Modul
  -> neue Datei / neues Modul
  -> Grund der Änderung
  -> Pattern / Architekturkonzept
  -> Test / Sicherheitsnetz
  -> Migrationsnutzen

Erste technische Modulgrenzen

Package-Regel

Alle Java-Beispiele verwenden:

package com.seb4u.demo.claims...

Beispiele:

com.seb4u.demo.claims.domain
com.seb4u.demo.claims.application
com.seb4u.demo.claims.application.port
com.seb4u.demo.claims.infrastructure.soap
com.seb4u.demo.claims.infrastructure.jms
com.seb4u.demo.claims.infrastructure.persistence
com.seb4u.demo.claims.workflow
com.seb4u.demo.claims.document
com.seb4u.demo.claims.payment
com.seb4u.demo.claims.audit
com.seb4u.demo.claims.search
com.seb4u.demo.claims.notification
com.seb4u.demo.claims.security

ModulabhÀngigkeiten als Regel

claim-domain
  darf keine AbhÀngigkeit auf EJB, SOAP, JMS, JDBC, JPA, Servlet, Spring, Jakarta Runtime haben.

claim-application
  darf domain verwenden.
  definiert Ports.
  darf keine konkreten SOAP/JMS/JDBC/File-Share-Implementierungen enthalten.

claim-infrastructure
  implementiert Ports.
  darf technische Frameworks verwenden.

claim-soap-api
  adaptiert Legacy SOAP Requests auf Application Commands.

claim-rest-api
  adaptiert moderne REST Requests auf Application Commands.

claim-batch
  ruft Application Use Cases fĂŒr Batch/SLA/Eskalation auf.

migration-tests
  prĂŒft Alt-/Neu-Verhalten, SOAP/REST-KompatibilitĂ€t und Smoke-Szenarien.

Erste Code-Skelette

Diese Code-Skelette sind bewusst noch nicht vollstĂ€ndig. Sie legen die Grenzen fest, die in Runde 2 und Runde 3 mit Legacy-Code, Monster Method und Refactoring gefĂŒllt werden.

Parent POM Grobstruktur

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.seb4u.demo.claims</groupId>
  <artifactId>legacy-claims-customer-support-enterprise</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>Legacy Claims & Customer Support Enterprise System</name>

  <modules>
    <module>after_refactoring_modular/shared-kernel</module>
    <module>after_refactoring_modular/claim-domain</module>
    <module>after_refactoring_modular/claim-application</module>
    <module>after_refactoring_modular/claim-document</module>
    <module>after_refactoring_modular/claim-workflow</module>
    <module>after_refactoring_modular/claim-payment</module>
    <module>after_refactoring_modular/claim-audit</module>
    <module>after_refactoring_modular/claim-search</module>
    <module>after_refactoring_modular/claim-notification</module>
    <module>after_refactoring_modular/claim-infrastructure</module>
    <module>after_refactoring_modular/claim-soap-api</module>
    <module>after_refactoring_modular/claim-rest-api</module>
    <module>after_refactoring_modular/claim-batch</module>
    <module>after_refactoring_modular/claim-openapi</module>
    <module>after_refactoring_modular/migration-tests</module>
  </modules>

  <properties>
    <java.version>17</java.version>
    <maven.compiler.release>${java.version}</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

Domain Package Boundary

package com.seb4u.demo.claims.domain;

/**
 * EnthĂ€lt reine Fachlogik fĂŒr Claims.
 *
 * Verboten:
 * - javax.ejb
 * - jakarta.ejb
 * - javax.xml.ws
 * - jakarta.ws.rs
 * - java.sql
 * - JMS APIs
 * - Servlet APIs
 * - WebSphere-spezifische APIs
 *
 * Erlaubt:
 * - Fachliche Entities
 * - Value Objects
 * - Policies
 * - State Machine
 * - Domain Events
 * - Fachliche Exceptions, sofern sie keine technische Infrastruktur verstecken
 */
public final class DomainBoundary {
    private DomainBoundary() {
    }
}

Application Port Beispiele

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

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.DocumentId;

public interface DocumentStoragePort {
    StoredDocumentReference storeClaimDocument(ClaimId claimId, UploadedClaimDocument document);

    ClaimDocumentContent loadClaimDocument(ClaimId claimId, DocumentId documentId);

    void archiveClaimDocument(ClaimId claimId, DocumentId documentId, ArchiveReason reason);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.RiskScore;

public interface FraudRiskPort {
    RiskScore evaluateFraudRisk(Claim claim);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.CoverageDecision;

public interface CoveragePort {
    CoverageDecision calculateCoverage(Claim claim);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.Money;

public interface PaymentReleasePort {
    PaymentPreparationResult preparePayment(ClaimId claimId, Money amount);

    PaymentReleaseResult releasePreparedPayment(ClaimId claimId, PaymentReference reference);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.AuditEvent;

public interface AuditLogPort {
    void append(AuditEvent event);

    ClaimAuditTrail loadTrail(ClaimId claimId);
}

Command Handler als Zielrichtung

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

import com.seb4u.demo.claims.application.port.AuditLogPort;
import com.seb4u.demo.claims.application.port.CoveragePort;
import com.seb4u.demo.claims.application.port.DocumentVerificationPort;
import com.seb4u.demo.claims.application.port.FraudRiskPort;
import com.seb4u.demo.claims.application.port.PaymentReleasePort;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.ClaimDecisionResult;
import com.seb4u.demo.claims.workflow.ClaimWorkflowOrchestrator;

public class ProcessClaimDecisionHandler {

    private final ClaimRepository claimRepository;
    private final CoveragePort coveragePort;
    private final FraudRiskPort fraudRiskPort;
    private final DocumentVerificationPort documentVerificationPort;
    private final PaymentReleasePort paymentReleasePort;
    private final AuditLogPort auditLogPort;
    private final ClaimWorkflowOrchestrator workflowOrchestrator;

    public ProcessClaimDecisionHandler(
            ClaimRepository claimRepository,
            CoveragePort coveragePort,
            FraudRiskPort fraudRiskPort,
            DocumentVerificationPort documentVerificationPort,
            PaymentReleasePort paymentReleasePort,
            AuditLogPort auditLogPort,
            ClaimWorkflowOrchestrator workflowOrchestrator
    ) {
        this.claimRepository = claimRepository;
        this.coveragePort = coveragePort;
        this.fraudRiskPort = fraudRiskPort;
        this.documentVerificationPort = documentVerificationPort;
        this.paymentReleasePort = paymentReleasePort;
        this.auditLogPort = auditLogPort;
        this.workflowOrchestrator = workflowOrchestrator;
    }

    public ClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Claim claim = claimRepository.loadForDecision(command.claimId());

        return workflowOrchestrator.processDecision(
                claim,
                command,
                coveragePort,
                fraudRiskPort,
                documentVerificationPort,
                paymentReleasePort,
                auditLogPort
        );
    }
}

Wichtig: In Runde 2 wird gezeigt, wie diese Struktur im Legacy-System noch nicht existiert. Dort steckt alles in LegacyClaimFacadeBean.processClaimDecision(...). In Runde 3 wird diese Zielrichtung Schritt fĂŒr Schritt aus der Legacy-Monster-Methode herausgearbeitet.

Legacy-Ausgangspunkt fĂŒr Runde 2

Runde 2 soll den schlechten Ausgangszustand sichtbar machen.

Erwartete Legacy-Dateien

legacy-websphere-ear/
  pom.xml
  src/main/application/META-INF/application.xml
  src/main/application/META-INF/ibm-application-bnd.xml

legacy-claims-ejb/
  src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimFacadeBean.java
  src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimDecisionServiceBean.java
  src/main/java/com/seb4u/demo/claims/legacy/dao/LegacyClaimJdbcDao.java
  src/main/java/com/seb4u/demo/claims/legacy/dao/AuditReportStoredProcedureDao.java
  src/main/java/com/seb4u/demo/claims/legacy/soap/FraudRiskSoapClient.java
  src/main/java/com/seb4u/demo/claims/legacy/soap/PolicyCoverageSoapClient.java
  src/main/java/com/seb4u/demo/claims/legacy/jms/LegacyNotificationSenderBean.java
  src/main/java/com/seb4u/demo/claims/legacy/security/JaasRoleChecker.java

legacy-claims-web/
  src/main/webapp/WEB-INF/web.xml
  src/main/webapp/WEB-INF/ibm-web-bnd.xml
  src/main/webapp/claimDecision.jsp
  src/main/java/com/seb4u/demo/claims/legacy/web/LegacyPartnerPortalServlet.java
  src/main/java/com/seb4u/demo/claims/legacy/web/InternalClaimsBackofficeServlet.java

Typische Monster-Method-Zutaten

processClaimDecision(...) soll spÀter bewusst enthalten:

Teststrategie als Zielbild

Testpyramide fĂŒr das Lernprojekt

               +-----------------------------+

               | wenige End-to-End Smoke     |
               +-----------------------------+
             +-------------------------------+

             | Migration / SOAP-REST Tests   |
             +-------------------------------+
           +---------------------------------+

           | Adapter / Contract Tests        |
           +---------------------------------+
         +-----------------------------------+

         | Application / Use Case Tests      |
         +-----------------------------------+
       +-------------------------------------+

       | Domain / Policy / State Tests       |
       +-------------------------------------+
     +---------------------------------------+

     | Characterization / Golden Master      |
     +---------------------------------------+

Testarten

TestartZweckZeitpunkt
Characterization TestLegacy-Verhalten dokumentierenvor Refactoring
Golden Master Testbekannte Inputs/Outputs sichernvor und wÀhrend Refactoring
SOAP Contract TestWSDL/Legacy-KompatibilitÀt sichernwÀhrend Migration
Application TestUse Cases unabhÀngig testennach Extraktion
Domain TestPolicies/State Machine prĂŒfennach Domain-Extraktion
Adapter TestSOAP/JMS/JDBC/File Share isoliert testenbei Port/Adapter
ArchUnit TestModulregeln absichernnach Modularisierung
Migration TestAlt/Neu vergleichenbei OpenShift Vorbereitung
Smoke TestRuntime grob prĂŒfennach Deployment

OpenShift-Zielbild

OpenShift ist in diesem Lernprojekt nicht nur eine technische Zielplattform. Es ist der Grund, warum Runtime-Grenzen sichtbar werden mĂŒssen.

Zu migrierende Runtime-Aspekte

LegacyOpenShift-Ziel
WebSphere Traditional EARContainerisierte Services
JNDI DataSourcesexterne Konfiguration / Secrets
WebSphere Scheduler / EJB TimerOpenShift CronJob / Batch Worker
lokale File SharesAdapter, spÀter Object Storage möglich
manuelle DeploymentsBuild Pipeline / Image / DeploymentConfig oder Deployment
technische Logsstrukturierte Logs, Correlation IDs
WebSphere RollenIdentity Provider, Security Policies
hardcodierte PropertiesConfigMap / Secret / Env Vars
direkte JMS-NebenwirkungenOutbox, Retry, Dead Letter
globale JTA-Annahmenklare Transaktionsgrenzen, Compensation

Zielordner fĂŒr Migration

migration_to_openshift/
  docker/
    Dockerfile.openliberty
    Dockerfile.springboot
  openshift/
    base/
      deployment.yaml
      service.yaml
      route.yaml
      configmap.yaml
      secret.example.yaml
    overlays/
      dev/
      test/
      prod/
  config/
    application-dev.yaml
    application-test.yaml
    application-prod.yaml
  runbooks/
    deployment_runbook.md
    rollback_runbook.md
    incident_runbook.md

Entscheidungen in Runde 1

Entscheidung 1: Legacy und Modern getrennt halten

Der Legacy-Ausgangspunkt bleibt sichtbar in before_refactoring_legacy/. Der moderne Zielzustand entsteht in after_refactoring_modular/.

Warum? Damit man didaktisch vergleichen kann, was sich wirklich geÀndert hat.

Entscheidung 2: Refactoring-Schritte separat versionieren

Die ZwischenstĂ€nde werden in refactoring_steps/ gefĂŒhrt.

Warum? Refactoring ist ein Lernprozess. Wenn nur Vorher und Nachher existieren, fehlt der wichtigste Teil.

Entscheidung 3: Zusatzsysteme bekommen eigene Legacy-Ordner

Alle Zusatzsysteme werden als eigene Legacy-Bausteine angelegt.

Warum? Im echten Enterprise-System liegen Probleme oft in Integrationen, nicht nur im Hauptsystem.

Entscheidung 4: Ports werden in Application bzw. Fachmodulen sichtbar gemacht

Externe Systeme werden ĂŒber Ports angebunden.

Warum? Damit Fachlogik testbar wird und Infrastruktur spÀter austauschbar ist.

Entscheidung 5: HTML spÀter ohne Pflicht-JavaScript

Die HTML-Version wird spÀter robust und offline-fÀhig gebaut.

Warum? Lokale HTML-Dateien auf iPhone/Safari können bei JavaScript-Buttons unzuverlÀssig sein.

3. Legacy-Ausgangssystem verstehen

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Legacy-Ausgangssystem verstehen in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Projekt: Legacy Claims & Customer Support Enterprise System Runde: 02 Datum: 2026-07-05 Arbeitsmodus: Rohmaterial, bewusst ausfĂŒhrlich, noch nicht konsolidiert Ziel dieser Runde: Das alte Ausgangssystem realistisch sichtbar machen, damit spĂ€tere Refactoring-, Test- und Migrationsschritte auf einem konkreten Legacy-Schnitt beruhen.

Einordnung dieser Runde

Runde 1 hat das Zielbild, die Systemlandschaft und die geplante Gesamtstruktur festgelegt. Runde 2 erzeugt jetzt den eigentlichen Legacy-Ausgangspunkt. Das ist wichtig, weil Refactoring nur dann didaktisch sinnvoll ist, wenn vorher sichtbar ist, wie schmutzig, gekoppelt und schwer testbar das alte System war.

Diese Runde ist daher absichtlich nicht „schön“. Viele Beispiele sind bewusst problematisch:

Das Ziel ist nicht, diesen Code zu empfehlen. Das Ziel ist, ein realistisches Ausgangsmaterial zu haben, das wir in spĂ€teren Runden Schritt fĂŒr Schritt retten.

Legacy-Gesamtbild vor Refactoring

Technische Sicht

Das alte System lÀuft auf:

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:

processClaimDecision(...)

Diese Methode macht fast alles:

Diese Methode wird in dieser Runde bewusst als Monster Method gezeigt.

Legacy-EAR-Struktur

Die alte Anwendung ist als EAR aufgebaut.

legacy-claims-customer-support-ear/
├── pom.xml
├── src/main/application/META-INF/
│   ├── application.xml
│   ├── ibm-application-bnd.xml
│   └── was.policy
├── legacy-claims-core-ejb.jar
├── legacy-claims-partner-portal.war
├── legacy-internal-claims-backoffice.war
├── legacy-document-management-adapter.jar
├── legacy-fraud-risk-gateway.jar
├── legacy-policy-coverage-system.jar
├── legacy-payment-release-system.jar
├── legacy-notification-system.jar
├── legacy-sla-escalation-scheduler.jar
├── legacy-audit-compliance-reporter.jar
├── legacy-search-indexer.jar
├── legacy-customer-master-data-system.jar
└── legacy-identity-role-management.jar

Legacy-Core-EJB-Struktur

legacy-claims-core-ejb/
├── src/main/java/com/seb4u/demo/claims/legacy/ejb/
│   ├── LegacyClaimFacadeBean.java
│   ├── LegacyClaimQueryBean.java
│   ├── LegacyClaimDocumentBean.java
│   ├── LegacyClaimPaymentBean.java
│   └── LegacyClaimAuditBean.java
├── src/main/java/com/seb4u/demo/claims/legacy/dao/
│   ├── LegacyClaimDao.java
│   ├── LegacyClaimSearchDao.java
│   ├── AuditReportStoredProcedureDao.java
│   └── LegacySequenceDao.java
├── src/main/java/com/seb4u/demo/claims/legacy/soap/
│   ├── LegacyClaimSoapEndpoint.java
│   ├── CustomerMasterDataSoapClient.java
│   ├── PolicyCoverageSoapClient.java
│   ├── FraudRiskSoapClient.java
│   ├── DocumentVerificationSoapClient.java
│   ├── PaymentReleaseSoapClient.java
│   └── NotificationSoapClient.java
├── src/main/java/com/seb4u/demo/claims/legacy/jms/
│   ├── LegacyClaimEventPublisher.java
│   └── LegacyClaimEventMessage.java
├── src/main/java/com/seb4u/demo/claims/legacy/security/
│   ├── JaasRoleChecker.java
│   └── LegacyLdapGroupResolver.java
├── src/main/java/com/seb4u/demo/claims/legacy/document/
│   ├── LegacyDocumentShareClient.java
│   └── LegacyDocumentPathBuilder.java
├── src/main/java/com/seb4u/demo/claims/legacy/scheduler/
│   └── SlaEscalationTimerBean.java
└── src/main/resources/META-INF/
    ├── ejb-jar.xml
    ├── ibm-ejb-jar-bnd.xml
    └── persistence.xml

Partner-Portal-WAR-Struktur

legacy-claims-partner-portal/
├── src/main/java/com/seb4u/demo/claims/legacy/portal/
│   ├── LegacyPartnerPortalServlet.java
│   ├── PartnerClaimUploadServlet.java
│   ├── PartnerClaimStatusServlet.java
│   └── PartnerPortalSessionFilter.java
├── src/main/webapp/
│   ├── WEB-INF/web.xml
│   ├── WEB-INF/ibm-web-bnd.xml
│   ├── WEB-INF/jsp/createClaim.jsp
│   ├── WEB-INF/jsp/uploadDocument.jsp
│   ├── WEB-INF/jsp/claimStatus.jsp
│   └── WEB-INF/jsp/messages.jsp
└── pom.xml

Internal-Backoffice-WAR-Struktur

legacy-internal-claims-backoffice/
├── src/main/java/com/seb4u/demo/claims/legacy/backoffice/
│   ├── InternalClaimsBackofficeServlet.java
│   ├── InternalClaimsBackofficeBean.java
│   ├── BackofficeSearchServlet.java
│   ├── BackofficeDecisionServlet.java
│   └── BackofficeSecurityFilter.java
├── src/main/webapp/
│   ├── WEB-INF/web.xml
│   ├── WEB-INF/ibm-web-bnd.xml
│   ├── WEB-INF/jsp/searchClaims.jsp
│   ├── WEB-INF/jsp/editClaim.jsp
│   ├── WEB-INF/jsp/reviewDocuments.jsp
│   ├── WEB-INF/jsp/decision.jsp
│   └── WEB-INF/jsp/auditTrail.jsp
└── pom.xml

WebSphere-Deskriptoren

application.xml

<application xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                                 http://xmlns.jcp.org/xml/ns/javaee/application_7.xsd"
             version="7">
    <display-name>Legacy Claims Customer Support EAR</display-name>

    <module>
        <ejb>legacy-claims-core-ejb.jar</ejb>
    </module>

    <module>
        <web>
            <web-uri>legacy-claims-partner-portal.war</web-uri>
            <context-root>/partner-claims</context-root>
        </web>
    </module>

    <module>
        <web>
            <web-uri>legacy-internal-claims-backoffice.war</web-uri>
            <context-root>/claims-backoffice</context-root>
        </web>
    </module>

    <module>
        <ejb>legacy-sla-escalation-scheduler.jar</ejb>
    </module>

    <library-directory>lib</library-directory>
</application>

Problematisch daran:

ibm-application-bnd.xml

<application-bnd
    xmlns="http://websphere.ibm.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://websphere.ibm.com/xml/ns/javaee
                        http://websphere.ibm.com/xml/ns/javaee/ibm-application-bnd_1_0.xsd"
    version="1.0">

    <security-role name="CLAIMS_AGENT">
        <group name="cn=claims-agents,ou=groups,dc=seb4u,dc=demo" />
    </security-role>

    <security-role name="CLAIMS_MANAGER">
        <group name="cn=claims-managers,ou=groups,dc=seb4u,dc=demo" />
    </security-role>

    <security-role name="PARTNER_USER">
        <group name="cn=partner-users,ou=groups,dc=seb4u,dc=demo" />
    </security-role>

    <security-role name="AUDITOR">
        <group name="cn=claims-auditors,ou=groups,dc=seb4u,dc=demo" />
    </security-role>
</application-bnd>

Problematisch daran:

ejb-jar.xml

<ejb-jar xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/ejb-jar_3_2.xsd"
         version="3.2">

    <enterprise-beans>
        <session>
            <ejb-name>LegacyClaimFacadeBean</ejb-name>
            <ejb-class>com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean</ejb-class>
            <session-type>Stateless</session-type>
            <transaction-type>Container</transaction-type>
        </session>
    </enterprise-beans>

    <assembly-descriptor>
        <method-permission>
            <role-name>CLAIMS_AGENT</role-name>
            <role-name>CLAIMS_MANAGER</role-name>
            <method>
                <ejb-name>LegacyClaimFacadeBean</ejb-name>
                <method-name>*</method-name>
            </method>
        </method-permission>

        <container-transaction>
            <method>
                <ejb-name>LegacyClaimFacadeBean</ejb-name>
                <method-name>processClaimDecision</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>
</ejb-jar>

Problematisch daran:

persistence.xml

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="claimsLegacyPU" transaction-type="JTA">
        <jta-data-source>jdbc/ClaimsOracleDS</jta-data-source>
        <class>com.seb4u.demo.claims.legacy.entity.LegacyClaimEntity</class>
        <class>com.seb4u.demo.claims.legacy.entity.LegacyClaimDocumentEntity</class>
        <class>com.seb4u.demo.claims.legacy.entity.LegacyClaimAuditEntity</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="false" />
        </properties>
    </persistence-unit>
</persistence>

Problematisch daran:

Legacy-Datenbankmodell und Stored Procedures

Wichtige Tabellen

CLAIM
CLAIM_DOCUMENT
CLAIM_AUDIT
CLAIM_STATUS_HISTORY
CLAIM_PAYMENT
CLAIM_ESCALATION
CLAIM_MESSAGE
CLAIM_SEARCH_CACHE
CLAIM_REPORTING_SNAPSHOT
PARTNER_ACCESS
CLAIM_ROLE_ASSIGNMENT

Typische Stored Procedures

CREATE OR REPLACE PROCEDURE SP_PROCESS_CLAIM_DECISION (
    p_claim_id            IN NUMBER,
    p_decision_code       IN VARCHAR2,
    p_actor_user          IN VARCHAR2,
    p_reason              IN VARCHAR2,
    p_new_status          OUT VARCHAR2,
    p_audit_id            OUT NUMBER
) AS
BEGIN
    UPDATE CLAIM
       SET STATUS = CASE
            WHEN p_decision_code = 'APPROVE' THEN 'APPROVED'
            WHEN p_decision_code = 'REJECT' THEN 'REJECTED'
            WHEN p_decision_code = 'ESCALATE' THEN 'ESCALATED'
            ELSE STATUS
       END,
       LAST_UPDATED_AT = SYSTIMESTAMP,
       LAST_UPDATED_BY = p_actor_user
     WHERE CLAIM_ID = p_claim_id;

    INSERT INTO CLAIM_AUDIT (
        AUDIT_ID,
        CLAIM_ID,
        ACTION_CODE,
        ACTOR_USER,
        DESCRIPTION,
        CREATED_AT
    ) VALUES (
        CLAIM_AUDIT_SEQ.NEXTVAL,
        p_claim_id,
        p_decision_code,
        p_actor_user,
        p_reason,
        SYSTIMESTAMP
    ) RETURNING AUDIT_ID INTO p_audit_id;

    SELECT STATUS INTO p_new_status
      FROM CLAIM
     WHERE CLAIM_ID = p_claim_id;
END;
/

Legacy-Problem:

Reporting Stored Procedure

CREATE OR REPLACE PROCEDURE SP_REFRESH_CLAIM_REPORTING AS
BEGIN
    DELETE FROM CLAIM_REPORTING_SNAPSHOT;

    INSERT INTO CLAIM_REPORTING_SNAPSHOT (
        CLAIM_ID,
        CUSTOMER_ID,
        STATUS,
        PARTNER_ID,
        CLAIM_AMOUNT,
        RISK_SCORE,
        SLA_BREACHED,
        LAST_ACTION,
        SNAPSHOT_CREATED_AT
    )
    SELECT c.CLAIM_ID,
           c.CUSTOMER_ID,
           c.STATUS,
           c.PARTNER_ID,
           c.CLAIM_AMOUNT,
           c.RISK_SCORE,
           CASE WHEN e.ESCALATION_ID IS NOT NULL THEN 'Y' ELSE 'N' END,
           a.ACTION_CODE,
           SYSTIMESTAMP
      FROM CLAIM c
      LEFT JOIN CLAIM_ESCALATION e ON e.CLAIM_ID = c.CLAIM_ID
      LEFT JOIN CLAIM_AUDIT a ON a.CLAIM_ID = c.CLAIM_ID
     WHERE c.CREATED_AT > SYSTIMESTAMP - 365;
END;
/

Legacy-Problem:

Legacy-SOAP-Operationen

Interne SOAP-Fassade

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

package com.seb4u.demo.claims.legacy.soap;

import javax.ejb.EJB;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;

@WebService(
    serviceName = "LegacyClaimService",
    portName = "LegacyClaimServicePort",
    targetNamespace = "http://legacy.claims.demo.seb4u.com"
)
public class LegacyClaimSoapEndpoint {

    @EJB
    private LegacyClaimFacadeBean claimFacade;

    @WebMethod(operationName = "createClaim")
    public LegacyCreateClaimResponse createClaim(@WebParam(name = "request") LegacyCreateClaimRequest request) {
        return claimFacade.createClaimFromSoap(request);
    }

    @WebMethod(operationName = "getClaimStatus")
    public LegacyClaimStatusResponse getClaimStatus(@WebParam(name = "request") LegacyClaimStatusRequest request) {
        return claimFacade.getClaimStatus(request);
    }

    @WebMethod(operationName = "submitClaimDocument")
    public LegacySubmitDocumentResponse submitClaimDocument(@WebParam(name = "request") LegacySubmitDocumentRequest request) {
        return claimFacade.submitClaimDocument(request);
    }

    @WebMethod(operationName = "processClaimDecision")
    public LegacyClaimDecisionResponse processClaimDecision(@WebParam(name = "request") LegacyClaimDecisionRequest request) {
        return claimFacade.processClaimDecision(request);
    }

    @WebMethod(operationName = "approveClaimPayment")
    public LegacyPaymentResponse approveClaimPayment(@WebParam(name = "request") LegacyPaymentRequest request) {
        return claimFacade.approveClaimPayment(request);
    }

    @WebMethod(operationName = "rejectClaim")
    public LegacyRejectClaimResponse rejectClaim(@WebParam(name = "request") LegacyRejectClaimRequest request) {
        return claimFacade.rejectClaim(request);
    }

    @WebMethod(operationName = "escalateClaim")
    public LegacyEscalationResponse escalateClaim(@WebParam(name = "request") LegacyEscalationRequest request) {
        return claimFacade.escalateClaim(request);
    }

    @WebMethod(operationName = "getClaimAuditTrail")
    public LegacyAuditTrailResponse getClaimAuditTrail(@WebParam(name = "request") LegacyAuditTrailRequest request) {
        return claimFacade.getClaimAuditTrail(request);
    }

    @WebMethod(operationName = "searchClaims")
    public LegacySearchClaimsResponse searchClaims(@WebParam(name = "request") LegacySearchClaimsRequest request) {
        return claimFacade.searchClaims(request);
    }

    @WebMethod(operationName = "getCustomerClaimHistory")
    public LegacyCustomerHistoryResponse getCustomerClaimHistory(@WebParam(name = "request") LegacyCustomerHistoryRequest request) {
        return claimFacade.getCustomerClaimHistory(request);
    }

    @WebMethod(operationName = "verifyClaimDocument")
    public LegacyVerifyDocumentResponse verifyClaimDocument(@WebParam(name = "request") LegacyVerifyDocumentRequest request) {
        return claimFacade.verifyClaimDocument(request);
    }

    @WebMethod(operationName = "calculateClaimCoverage")
    public LegacyCoverageResponse calculateClaimCoverage(@WebParam(name = "request") LegacyCoverageRequest request) {
        return claimFacade.calculateClaimCoverage(request);
    }
}

Problematisch daran:

JMS und IBM MQ im Legacy-System

JMS Publisher

package com.seb4u.demo.claims.legacy.jms;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;

@Stateless
public class LegacyClaimEventPublisher {

    @Resource(lookup = "jms/ClaimsConnectionFactory")
    private ConnectionFactory connectionFactory;

    @Resource(lookup = "jms/ClaimsEventsQueue")
    private Queue claimsEventsQueue;

    public void publishDecisionChanged(Long claimId, String oldStatus, String newStatus, String actor, String correlationId) {
        Connection connection = null;
        Session session = null;
        try {
            connection = connectionFactory.createConnection();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            MessageProducer producer = session.createProducer(claimsEventsQueue);

            String payload = "{" +
                "\"eventType\":\"CLAIM_DECISION_CHANGED\"," +
                "\"claimId\":" + claimId + "," +
                "\"oldStatus\":\"" + oldStatus + "\"," +
                "\"newStatus\":\"" + newStatus + "\"," +
                "\"actor\":\"" + actor + "\"," +
                "\"correlationId\":\"" + correlationId + "\"" +
                "}";

            TextMessage message = session.createTextMessage(payload);
            message.setStringProperty("eventType", "CLAIM_DECISION_CHANGED");
            message.setStringProperty("correlationId", correlationId);
            producer.send(message);
        } catch (JMSException e) {
            throw new IllegalStateException("Could not publish claim event", e);
        } finally {
            try {
                if (session != null) session.close();
                if (connection != null) connection.close();
            } catch (JMSException ignored) {
                // ignored in legacy system
            }
        }
    }
}

Legacy-Probleme:

WebSphere JMS Bindings

<ejb-jar-bnd xmlns="http://websphere.ibm.com/xml/ns/javaee"
             version="1.0">
    <message-destination-ref name="jms/ClaimsEventsQueue"
        binding-name="jms/ClaimsEventsQueue" />
    <resource-ref name="jms/ClaimsConnectionFactory"
        binding-name="jms/ClaimsConnectionFactory" />
</ejb-jar-bnd>

Security: LDAP, JAAS und hart kodierte Rollen

JaasRoleChecker.java

package com.seb4u.demo.claims.legacy.security;

import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import java.util.HashSet;
import java.util.Set;

@Stateless
public class JaasRoleChecker {

    @Resource
    private SessionContext sessionContext;

    public boolean canProcessDecision(String claimStatus, String decisionCode, String amountCategory) {
        if (sessionContext.isCallerInRole("CLAIMS_MANAGER")) {
            return true;
        }

        if (sessionContext.isCallerInRole("CLAIMS_AGENT")) {
            if ("LOW".equals(amountCategory) && "APPROVE".equals(decisionCode)) {
                return true;
            }
            if ("REJECT".equals(decisionCode) && ("NEW".equals(claimStatus) || "IN_REVIEW".equals(claimStatus))) {
                return true;
            }
        }

        if (sessionContext.isCallerInRole("PARTNER_USER")) {
            return false;
        }

        return false;
    }

    public Set<String> currentRoles() {
        Set<String> roles = new HashSet<>();
        if (sessionContext.isCallerInRole("CLAIMS_AGENT")) roles.add("CLAIMS_AGENT");
        if (sessionContext.isCallerInRole("CLAIMS_MANAGER")) roles.add("CLAIMS_MANAGER");
        if (sessionContext.isCallerInRole("PARTNER_USER")) roles.add("PARTNER_USER");
        if (sessionContext.isCallerInRole("AUDITOR")) roles.add("AUDITOR");
        return roles;
    }
}

Legacy-Probleme:

Document Management Adapter im Legacy-System

LegacyDocumentShareClient.java

package com.seb4u.demo.claims.legacy.document;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class LegacyDocumentShareClient {

    private static final String ROOT = "//legacy-fileserver01/claims-documents";

    public String storeDocument(Long claimId, String documentType, String fileName, File uploadedFile) {
        String folder = ROOT + "/" + claimId + "/" + documentType;
        File directory = new File(folder);
        if (!directory.exists() && !directory.mkdirs()) {
            throw new IllegalStateException("Could not create document folder " + folder);
        }

        String targetPath = folder + "/" + System.currentTimeMillis() + "_" + fileName;
        try {
            Files.copy(uploadedFile.toPath(), Path.of(targetPath), StandardCopyOption.REPLACE_EXISTING);
            return targetPath;
        } catch (IOException e) {
            throw new IllegalStateException("Could not store document " + fileName, e);
        }
    }

    public byte[] readDocument(String absolutePath) {
        try (FileInputStream in = new FileInputStream(absolutePath)) {
            return in.readAllBytes();
        } catch (IOException e) {
            throw new IllegalStateException("Could not read document " + absolutePath, e);
        }
    }

    public boolean exists(String absolutePath) {
        return new File(absolutePath).exists();
    }
}

Legacy-Probleme:

Legacy-DAO mit JDBC, JPA und Stored Procedures

LegacyClaimDao.java

package com.seb4u.demo.claims.legacy.dao;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.sql.DataSource;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

@Stateless
public class LegacyClaimDao {

    @PersistenceContext(unitName = "claimsLegacyPU")
    private EntityManager entityManager;

    @Resource(lookup = "jdbc/ClaimsOracleDS")
    private DataSource dataSource;

    public LegacyClaimRecord findByIdForUpdate(Long claimId) {
        try (Connection connection = dataSource.getConnection();
             CallableStatement statement = connection.prepareCall(
                 "select CLAIM_ID, STATUS, CUSTOMER_ID, POLICY_NO, CLAIM_AMOUNT, RISK_SCORE, " +
                 "       PARTNER_ID, DOCUMENT_STATUS, PAYMENT_STATUS, SLA_DUE_AT " +
                 "  from CLAIM where CLAIM_ID = ? for update")) {

            statement.setLong(1, claimId);
            try (ResultSet rs = statement.executeQuery()) {
                if (!rs.next()) {
                    return null;
                }
                return new LegacyClaimRecord(
                    rs.getLong("CLAIM_ID"),
                    rs.getString("STATUS"),
                    rs.getString("CUSTOMER_ID"),
                    rs.getString("POLICY_NO"),
                    rs.getBigDecimal("CLAIM_AMOUNT"),
                    rs.getBigDecimal("RISK_SCORE"),
                    rs.getString("PARTNER_ID"),
                    rs.getString("DOCUMENT_STATUS"),
                    rs.getString("PAYMENT_STATUS"),
                    rs.getTimestamp("SLA_DUE_AT")
                );
            }
        } catch (SQLException e) {
            throw new IllegalStateException("Could not load claim " + claimId, e);
        }
    }

    public StoredProcedureDecisionResult callProcessClaimDecision(Long claimId, String decisionCode, String actor, String reason) {
        try (Connection connection = dataSource.getConnection();
             CallableStatement statement = connection.prepareCall("{call SP_PROCESS_CLAIM_DECISION(?,?,?,?,?,?)}")) {
            statement.setLong(1, claimId);
            statement.setString(2, decisionCode);
            statement.setString(3, actor);
            statement.setString(4, reason);
            statement.registerOutParameter(5, Types.VARCHAR);
            statement.registerOutParameter(6, Types.NUMERIC);
            statement.execute();
            return new StoredProcedureDecisionResult(
                statement.getString(5),
                statement.getLong(6)
            );
        } catch (SQLException e) {
            throw new IllegalStateException("Could not call SP_PROCESS_CLAIM_DECISION", e);
        }
    }

    public void updateRiskScore(Long claimId, BigDecimal riskScore) {
        entityManager.createNativeQuery(
            "update CLAIM set RISK_SCORE = :riskScore, LAST_UPDATED_AT = SYSTIMESTAMP where CLAIM_ID = :claimId")
            .setParameter("riskScore", riskScore)
            .setParameter("claimId", claimId)
            .executeUpdate();
    }

    public void markDocumentStatus(Long claimId, String documentStatus) {
        entityManager.createNativeQuery(
            "update CLAIM set DOCUMENT_STATUS = :status where CLAIM_ID = :claimId")
            .setParameter("status", documentStatus)
            .setParameter("claimId", claimId)
            .executeUpdate();
    }
}

Legacy-Probleme:

Legacy Partner Portal

LegacyPartnerPortalServlet.java

package com.seb4u.demo.claims.legacy.portal;

import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.soap.LegacyCreateClaimRequest;
import com.seb4u.demo.claims.legacy.soap.LegacyCreateClaimResponse;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@MultipartConfig
public class LegacyPartnerPortalServlet extends HttpServlet {

    @EJB
    private LegacyClaimFacadeBean claimFacade;

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String partnerId = (String) request.getSession().getAttribute("partnerId");
        String customerId = request.getParameter("customerId");
        String policyNo = request.getParameter("policyNo");
        String incidentText = request.getParameter("incidentText");
        String amount = request.getParameter("claimAmount");

        LegacyCreateClaimRequest legacyRequest = new LegacyCreateClaimRequest();
        legacyRequest.setPartnerId(partnerId);
        legacyRequest.setCustomerId(customerId);
        legacyRequest.setPolicyNo(policyNo);
        legacyRequest.setIncidentDescription(incidentText);
        legacyRequest.setClaimAmount(amount);
        legacyRequest.setSourceSystem("PARTNER_PORTAL_JSP");

        try {
            LegacyCreateClaimResponse legacyResponse = claimFacade.createClaimFromSoap(legacyRequest);
            request.setAttribute("claimId", legacyResponse.getClaimId());
            request.setAttribute("message", "Schaden wurde erfasst");
            request.getRequestDispatcher("/WEB-INF/jsp/claimStatus.jsp").forward(request, response);
        } catch (Exception ex) {
            request.setAttribute("error", ex.getMessage());
            request.getRequestDispatcher("/WEB-INF/jsp/createClaim.jsp").forward(request, response);
        }
    }
}

Legacy-Probleme:

Legacy Internal Claims Backoffice

InternalClaimsBackofficeBean.java

package com.seb4u.demo.claims.legacy.backoffice;

import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.soap.LegacyClaimDecisionRequest;
import com.seb4u.demo.claims.legacy.soap.LegacyClaimDecisionResponse;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import java.util.Map;

@Stateless
public class InternalClaimsBackofficeBean {

    @EJB
    private LegacyClaimFacadeBean claimFacade;

    public LegacyClaimDecisionResponse submitDecisionForm(Map<String, String> form, String currentUser) {
        LegacyClaimDecisionRequest request = new LegacyClaimDecisionRequest();
        request.setClaimId(Long.valueOf(form.get("claimId")));
        request.setDecisionCode(form.get("decisionCode"));
        request.setReason(form.get("reason"));
        request.setActorUser(currentUser);
        request.setForcePayment("on".equals(form.get("forcePayment")));
        request.setManualOverride("on".equals(form.get("manualOverride")));
        request.setBackofficeComment(form.get("comment"));
        request.setCorrelationId(form.get("correlationId"));
        return claimFacade.processClaimDecision(request);
    }
}

Legacy-Probleme:

Legacy Zusatzsysteme als konkrete technische Adapter

FraudRiskSoapClient.java

package com.seb4u.demo.claims.legacy.soap;

import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;

public class FraudRiskSoapClient {

    @WebServiceRef(wsdlLocation = "META-INF/wsdl/FraudRiskService.wsdl")
    private FraudRiskService fraudRiskService;

    public FraudRiskLegacyResponse checkRisk(Long claimId, String customerId, String policyNo, BigDecimal amount) {
        FraudRiskLegacyRequest request = new FraudRiskLegacyRequest();
        request.setClaimId(String.valueOf(claimId));
        request.setCustomerId(customerId);
        request.setPolicyNumber(policyNo);
        request.setClaimAmount(amount.toPlainString());
        request.setRequestMode("FULL_SYNC_CHECK");
        request.setCaller("LEGACY_CLAIMS_EJB");

        try {
            return fraudRiskService.getFraudRiskServicePort().checkRisk(request);
        } catch (Exception e) {
            FraudRiskLegacyResponse fallback = new FraudRiskLegacyResponse();
            fallback.setScore("999");
            fallback.setRiskCategory("TECHNICAL_ERROR_TREATED_AS_HIGH_RISK");
            fallback.setManualReviewRequired(true);
            fallback.setMessage(e.getMessage());
            return fallback;
        }
    }
}

Problem:

PolicyCoverageSoapClient.java

package com.seb4u.demo.claims.legacy.soap;

import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;

public class PolicyCoverageSoapClient {

    @WebServiceRef(wsdlLocation = "META-INF/wsdl/PolicyCoverageService.wsdl")
    private PolicyCoverageService policyCoverageService;

    public LegacyCoverageResult calculateCoverage(String policyNo, String claimType, BigDecimal amount) {
        LegacyCoverageRequest request = new LegacyCoverageRequest();
        request.setPolicyNumber(policyNo);
        request.setClaimType(claimType);
        request.setRequestedAmount(amount.toPlainString());
        request.setLegacyTariffMode("Y");

        return policyCoverageService.getPolicyCoverageServicePort().calculateClaimCoverage(request);
    }
}

Problem:

PaymentReleaseSoapClient.java

package com.seb4u.demo.claims.legacy.soap;

import javax.xml.ws.WebServiceRef;
import java.math.BigDecimal;

public class PaymentReleaseSoapClient {

    @WebServiceRef(wsdlLocation = "META-INF/wsdl/PaymentReleaseService.wsdl")
    private PaymentReleaseService paymentReleaseService;

    public LegacyPaymentReleaseResponse releasePayment(Long claimId, String customerId, BigDecimal amount, String actor) {
        LegacyPaymentReleaseRequest request = new LegacyPaymentReleaseRequest();
        request.setClaimReference("CLAIM-" + claimId);
        request.setCustomerNumber(customerId);
        request.setAmount(amount.toPlainString());
        request.setCurrency("EUR");
        request.setReleasedBy(actor);
        request.setReason("CLAIM_APPROVED");
        return paymentReleaseService.getPaymentReleaseServicePort().releasePayment(request);
    }
}

Problem:

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

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

LegacyClaimFacadeBean.java

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

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

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

@Stateless
public class LegacyClaimFacadeBean {

    @EJB
    private LegacyClaimDao claimDao;

    @EJB
    private LegacyClaimAuditBean auditBean;

    @EJB
    private LegacyClaimDocumentBean documentBean;

    @EJB
    private LegacyClaimPaymentBean paymentBean;

    @EJB
    private JaasRoleChecker roleChecker;

    @EJB
    private LegacyClaimEventPublisher eventPublisher;

    @EJB
    private LegacyClaimSearchBean searchBean;

    @EJB
    private LegacyNotificationSenderBean notificationSender;

    @Resource
    private SessionContext sessionContext;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Warum diese Methode schlecht ist

Diese Methode verletzt fast jedes wichtige Refactoring-Prinzip:

ProblemBeispielFolge
Zu viele VerantwortlichkeitenSecurity, Dokumente, Payment, Audit, JMS, Searchschwer testbar
Fachlogik in EJBStatusregeln direkt in BeanWebSphere-AbhÀngigkeit
Externe Systeme direktSOAP-Clients im Ablaufkeine Ports/Adapter
Transaktion zu großJTA ĂŒber DB, SOAP, JMSSeiteneffekte schwer kontrollierbar
Fehlerlogik chaotischIllegalStateException fĂŒr FachfĂ€llekeine klare Fehlersemantik
Statuslogik verteiltJava + Stored Procedureschwer nachvollziehbar
Rollen hart kodiertCLAIMS_MANAGER im CodeSecurity-Migration schwer
Dokumente hart gekoppeltFile Share direktObject Storage schwer
Payment synchronFreigabe im Entscheidungsablaufkeine Idempotenz
Event ohne OutboxJMS direktInkonsistenz möglich
Tests schwierigEJB/JNDI/SOAP/JMS nötigkaum Unit Tests möglich

Erste Refactoring-Kandidaten aus der Monster Method

BereichSpÀterer ZielbausteinPattern / Konzept
Request-ValidierungProcessClaimDecisionCommandCommand Pattern
RollenprĂŒfungAuthorizationPolicyPolicy Object
StatuswechselClaimState, ClaimTransitionPolicyState Pattern
CoverageCoveragePortPort & Adapter
FraudFraudRiskPortAnti-Corruption Layer
DokumentprĂŒfungDocumentVerificationPortPort & Adapter
PaymentPaymentReleasePortOutbox, Idempotency
AuditAuditLogPortAudit Boundary
NotificationNotificationPortOutbox / Adapter
SucheClaimSearchPortRead Model / Adapter
SLA/EskalationEscalationPolicyBatch Worker
OrchestrierungClaimDecisionWorkflowOrchestratorWorkflow Orchestrator

Legacy SLA & Escalation Scheduler

SlaEscalationTimerBean.java

package com.seb4u.demo.claims.legacy.scheduler;

import com.seb4u.demo.claims.legacy.dao.LegacyClaimDao;
import com.seb4u.demo.claims.legacy.ejb.LegacyNotificationSenderBean;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TimerService;
import java.util.List;

@Singleton
@Startup
public class SlaEscalationTimerBean {

    @Resource
    private TimerService timerService;

    @EJB
    private LegacyClaimDao claimDao;

    @EJB
    private LegacyNotificationSenderBean notificationSender;

    @PostConstruct
    public void init() {
        // In production, this bean sometimes starts twice during deployment restart.
    }

    @Schedule(hour = "*/1", minute = "0", persistent = false)
    public void checkOpenClaimsAndEscalate() {
        List<Long> overdueClaims = claimDao.findClaimsWithBreachedSla();
        for (Long claimId : overdueClaims) {
            try {
                claimDao.markClaimEscalatedByScheduler(claimId, "SLA_BREACHED");
                claimDao.insertSchedulerAudit(claimId, "SLA escalation created by WebSphere TimerBean");
                notificationSender.sendEscalationNotification(claimId, "SYSTEM", "SLA breached");
            } catch (Exception ex) {
                // Legacy: log only, continue. No retry model, no dead letter, no lease lock.
                System.err.println("Could not escalate claim " + claimId + ": " + ex.getMessage());
            }
        }
    }
}

Legacy-Probleme:

Legacy Search Indexer

LegacyClaimSearchDao.java

package com.seb4u.demo.claims.legacy.dao;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

@Stateless
public class LegacyClaimSearchDao {

    @Resource(lookup = "jdbc/ClaimsOracleDS")
    private DataSource dataSource;

    public List<LegacyClaimSearchRow> search(String freeText, String status, String partnerId) {
        String sql = "select CLAIM_ID, CUSTOMER_ID, POLICY_NO, STATUS, PARTNER_ID, CLAIM_AMOUNT " +
                     "from CLAIM " +
                     "where (upper(CUSTOMER_ID) like upper(?) " +
                     "   or upper(POLICY_NO) like upper(?) " +
                     "   or upper(PARTNER_ID) like upper(?)) ";

        if (status != null && !status.trim().isEmpty()) {
            sql += " and STATUS = '" + status + "' ";
        }
        if (partnerId != null && !partnerId.trim().isEmpty()) {
            sql += " and PARTNER_ID = '" + partnerId + "' ";
        }
        sql += " order by LAST_UPDATED_AT desc";

        try (Connection c = dataSource.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            String pattern = "%" + freeText + "%";
            ps.setString(1, pattern);
            ps.setString(2, pattern);
            ps.setString(3, pattern);

            try (ResultSet rs = ps.executeQuery()) {
                List<LegacyClaimSearchRow> rows = new ArrayList<>();
                while (rs.next()) {
                    rows.add(new LegacyClaimSearchRow(
                        rs.getLong("CLAIM_ID"),
                        rs.getString("CUSTOMER_ID"),
                        rs.getString("POLICY_NO"),
                        rs.getString("STATUS"),
                        rs.getString("PARTNER_ID"),
                        rs.getBigDecimal("CLAIM_AMOUNT")
                    ));
                }
                return rows;
            }
        } catch (Exception e) {
            throw new IllegalStateException("Search failed", e);
        }
    }
}

Legacy-Probleme:

Legacy Audit & Compliance Reporter

AuditReportStoredProcedureDao.java

package com.seb4u.demo.claims.legacy.dao;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.sql.DataSource;
import java.io.FileWriter;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;

@Stateless
public class AuditReportStoredProcedureDao {

    @Resource(lookup = "jdbc/ClaimsOracleDS")
    private DataSource dataSource;

    public String exportMonthlyAuditCsv(String month) {
        String path = "//legacy-fileserver01/claims-reports/audit/audit_" + month + ".csv";
        try (Connection connection = dataSource.getConnection();
             CallableStatement statement = connection.prepareCall("{call SP_EXPORT_AUDIT_REPORT(?)}");
             FileWriter writer = new FileWriter(path)) {
            statement.setString(1, month);
            boolean hasResultSet = statement.execute();
            if (hasResultSet) {
                try (ResultSet rs = statement.getResultSet()) {
                    writer.write("claimId;action;actor;createdAt;description\n");
                    while (rs.next()) {
                        writer.write(rs.getLong("CLAIM_ID") + ";" +
                                     rs.getString("ACTION_CODE") + ";" +
                                     rs.getString("ACTOR_USER") + ";" +
                                     rs.getTimestamp("CREATED_AT") + ";" +
                                     rs.getString("DESCRIPTION") + "\n");
                    }
                }
            }
            return path;
        } catch (Exception e) {
            throw new IllegalStateException("Could not export audit report", e);
        }
    }
}

Legacy-Probleme:

Legacy Notification System

LegacyNotificationSenderBean.java

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

import javax.ejb.Stateless;
import java.nio.file.Files;
import java.nio.file.Path;

@Stateless
public class LegacyNotificationSenderBean {

    private static final String TEMPLATE_ROOT = "//legacy-fileserver01/claims-notification-templates";

    public void sendClaimApprovedNotification(Long claimId, String customerId, String partnerId) {
        String template = readTemplate("claim_approved.txt");
        String text = template
            .replace("${claimId}", String.valueOf(claimId))
            .replace("${customerId}", customerId)
            .replace("${partnerId}", partnerId);
        sendMail(customerId, "Schaden genehmigt", text);
        sendPartnerMessage(partnerId, text);
    }

    public void sendClaimRejectedNotification(Long claimId, String customerId, String partnerId, String reason) {
        String template = readTemplate("claim_rejected.txt");
        String text = template
            .replace("${claimId}", String.valueOf(claimId))
            .replace("${reason}", reason);
        sendMail(customerId, "Schaden abgelehnt", text);
        sendPartnerMessage(partnerId, text);
    }

    public void sendEscalationNotification(Long claimId, String actor, String reason) {
        String template = readTemplate("claim_escalated.txt");
        String text = template
            .replace("${claimId}", String.valueOf(claimId))
            .replace("${actor}", actor)
            .replace("${reason}", reason);
        sendMail("claims-teamlead@legacy.local", "Eskalation", text);
    }

    public void sendMoreInfoRequest(Long claimId, String partnerId, String message) {
        sendPartnerMessage(partnerId, "Bitte weitere Informationen zu Schaden " + claimId + ": " + message);
    }

    private String readTemplate(String file) {
        try {
            return Files.readString(Path.of(TEMPLATE_ROOT, file));
        } catch (Exception e) {
            throw new IllegalStateException("Could not read notification template " + file, e);
        }
    }

    private void sendMail(String receiver, String subject, String text) {
        // Legacy SMTP gateway call hidden here
    }

    private void sendPartnerMessage(String partnerId, String text) {
        // Legacy partner message table insert hidden here
    }
}

Legacy-Probleme:

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 / ModulHauptproblemSpÀteres Ziel
LegacyClaimFacadeBean.javaMonster Method, EJB-ZentralismusProcessClaimDecisionHandler, ClaimDecisionWorkflowOrchestrator
LegacyClaimStatusIfElse.javaStatuslogik in if/elseClaimState, ClaimTransitionPolicy
LegacyDocumentShareClient.javaFile Share hart verdrahtetDocumentStoragePort, FileShareDocumentAdapter
FraudRiskSoapClient.javadirekter SOAP-AufrufFraudRiskPort, FraudRiskSoapAdapter
PolicyCoverageSoapClient.javaCoverage-Logik im ClientCoveragePort, CoverageSoapAdapter, CoveragePolicy
PaymentReleaseSoapClient.javasynchrone Zahlung ohne IdempotenzPaymentReleasePort, PaymentReleaseSoapAdapter, Outbox
LegacyNotificationSenderBean.javadirekte BenachrichtigungNotificationPort, NotificationAdapter
SlaEscalationTimerBean.javaTimerBean ohne Lease LockEscalationBatchJob, EscalationPolicy
AuditReportStoredProcedureDao.javaReporting ĂŒber Stored ProcedureAuditLogPort, AuditReportingAdapter
LegacyClaimSearchDao.javaOracle LIKE SucheClaimSearchPort, SearchIndexAdapter
CustomerMasterDataSoapClient.javadirekte KundendatenkopplungCustomerMasterDataPort, CustomerMasterDataSoapAdapter
JaasRoleChecker.javaWebSphere/JAAS-Rollen im CodeSecurityContextPort, AuthorizationPolicy
LegacyPartnerPortalServlet.javaJSP/Servlet an EJB gekoppeltPartnerPortalRestController, PartnerPortalBffAdapter
InternalClaimsBackofficeBean.javaBackoffice an SOAP-DTO gekoppeltClaimBackofficeUseCase

Testprobleme im Legacy-Ausgangssystem

Warum kaum Unit Tests existieren

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:

TestfallErwartung
APPROVE mit hoher Fraud Risk ohne Overridefachliche Ablehnung
APPROVE ohne Dokumentefachliche Ablehnung
REJECT mit kurzer BegrĂŒndungValidierungsfehler
ESCALATE ohne GrundDefault-Grund wird gesetzt
Coverage nicht gedeckt + APPROVEfachliche Ablehnung
Payment Service Fehler nach ApprovalTransaktions-/Seiteneffektproblem sichtbar machen
JMS Fehler nach StatusupdateInkonsistenz sichtbar machen
Fraud Service TimeoutLegacy-Fallback dokumentieren

Risiken, die vor Refactoring verstanden werden mĂŒssen

RisikoWarum kritisch?Sicherheitsnetz in spÀterer Runde
Statuslogik verteiltJava und PL/SQL Àndern StatusGolden Master + State Transition Tests
Payment-SeiteneffekteZahlung kann ausgelöst sein, obwohl DB rollbacktIdempotency + Compensation Tests
JMS ohne OutboxEvent kann fehlen oder doppelt seinOutbox Tests
Dokumente als File PathsMigration zu Object Storage schwierigAdapter Tests
Security in JAASOpenShift/OIDC Migration schwierigAuthorizationPolicy Tests
Stored ProceduresFachlogik schwer sichtbarCharacterization Tests gegen Test-DB/Fake
Partner Portal KopplungUI-Strangler schwierigContract Tests
Backoffice KopplungSachbearbeiter-Workflow schwer ablösbarUse Case Tests

Runde-2-Ergebnis

Diese Runde hat das Legacy-Ausgangssystem konkretisiert:

Das ist jetzt die Grundlage fĂŒr Runde 3: Refactoring-Lerninhalt Schritt fĂŒr Schritt.

Erzeugte Kapitel / Module dieser Runde

Wichtige Codebeispiele dieser Runde

4. Refactoring Schritt fĂŒr Schritt

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Refactoring Schritt fĂŒr Schritt in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Projekt: Legacy Claims & Customer Support Enterprise System

Runde: 03

Thema: Refactoring der Legacy-Claims-Monster-Method und der gekoppelten Systemlandschaft Schritt fĂŒr Schritt

Datum / Reihenfolge: 2026-07-05 / Runde 03

Ziel dieser Runde

Diese Runde konzentriert sich auf den wichtigsten Lernteil des Projekts: Wie wird ein altes, eng gekoppeltes Java-Enterprise-System so refactored, dass es verstÀndlich, testbar, modularisierbar und spÀter nach OpenShift migrierbar wird?

Im Mittelpunkt steht die Legacy-Monster-Method:

processClaimDecision(...)

Aus Runde 2 kennen wir die typischen Probleme:

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

Die Refactoring-Schritte dieser Runde sind:

SchrittTitelHauptzielPattern / Konzept
01Inventory und CharacterizationVerhalten erfassenCharacterization Test, Golden Master
02Refactoring Seam einfĂŒhrenMethode aufrufbar und messbar machenSprout Method, Facade Stabilisierung
03Command Object extrahierenParameterchaos reduzierenCommand Pattern, DTO
04Ergebnisobjekt einfĂŒhrentechnische Exceptions reduzierenResult Object
05Statuslogik isolierenif/else-Ketten herausziehenState Pattern, Policy Object
06Dokumentenlogik extrahierenFile Share entkoppelnPort, Adapter, Anti-Corruption Layer
07Coverage/Fraud/Payment als Portsexterne SOAP-Kopplung lösenPorts & Adapters
08Customer/Identity PortsStammdaten und Rollen abstrahierenSecurity Context Port, ACL
09Workflow OrchestratorAblauf steuerbar machenWorkflow Orchestrator, Application Service
10Audit Trail abstrahierenversteckte Audit-Logik sichtbar machenAudit Port, Domain Event
11Notification/Search/Reporting trennenNebenwirkungen entfernenOutbox, Read Model
12Idempotency und OutboxWiederholbarkeit schaffenIdempotency Key, Outbox Pattern
13Batch/SLA trennenScheduler aus Fachlogik lösenBatch Worker, Lease Lock
14Legacy UI entkoppelnPartner Portal und Backoffice ablösenStrangler UI, REST/BFF Boundary

Ausgangspunkt: problematische Legacy-Verantwortungen

Die alte LegacyClaimFacadeBean ist nicht nur eine Fassade. Sie ist gleichzeitig:

Das ist ein klassisches Enterprise-Anti-Pattern:

Eine technische Fassade wird mit der Zeit zum fachlichen Zentralhirn.

Problematische Verantwortungskarte

VerantwortungLegacy-OrtProblemZiel-Ort nach Refactoring
Claim-EntscheidungLegacyClaimFacadeBeanMonster MethodProcessClaimDecisionHandler
StatusĂŒbergĂ€ngeif/else in EJBuntestbarClaimTransitionPolicy
DokumentenprĂŒfungdirekter File/SOAP-Aufrufharte InfrastrukturkopplungDocumentVerificationPort
CoverageSOAP Client direkttechnische KopplungCoveragePort
FraudSOAP Client direktTimeouts als FachflussFraudRiskPort
PaymentSOAP/JMS direktTransaktionsmischungPaymentReleasePort + Outbox
AuditStored ProcedureverstecktAuditLogPort
NotificationJMS/SOAP direktNebenwirkung in TransaktionNotificationPort + Outbox
RollenprĂŒfungJAAS direkthart kodiertSecurityContextPort + AuthorizationPolicy
SearchOracle LIKEschlechte PerformanceClaimSearchPort / 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:

Was wird geÀndert?

Noch nichts am Produktivcode. Zuerst wird beobachtet.

Wir erstellen:

Pattern / Konzept

Tests / Sicherheitsnetz

Tests prĂŒfen zunĂ€chst nicht, ob das Legacy-Verhalten schön ist. Sie prĂŒfen, ob das aktuelle Verhalten stabil reproduzierbar ist.

Risiko

Characterization Tests können schlechtes Verhalten konservieren. Deshalb werden sie spÀter schrittweise von fachlich sauberen Tests ersetzt.

Testdatenmodell fĂŒr Characterization Tests

Datei:

migration-tests/src/test/resources/golden-master/claim-decision-cases.yml
cases:
  - id: CLAIM_APPROVE_SIMPLE
    claimId: CLM-10001
    customerId: CUST-4711
    policyNumber: POL-2024-0001
    currentStatus: PENDING_REVIEW
    requestedDecision: APPROVE
    actor: claims.manager
    roles:
      - CLAIMS_MANAGER
    documents:
      - type: DAMAGE_PHOTO
        verified: true
      - type: INVOICE
        verified: true
    fraudScore: 12
    coverage:
      covered: true
      deductibleAmount: 250.00
      maxPayableAmount: 5000.00
    expectedLegacyStatus: PAYMENT_PENDING
    expectedPaymentPrepared: true
    expectedAuditEvents:
      - CLAIM_DECISION_STARTED
      - COVERAGE_CHECKED
      - FRAUD_CHECKED
      - DOCUMENTS_VERIFIED
      - PAYMENT_PREPARED
      - CLAIM_APPROVED

  - id: CLAIM_REJECT_FRAUD_HIGH_RISK
    claimId: CLM-10002
    customerId: CUST-4712
    policyNumber: POL-2024-0002
    currentStatus: PENDING_REVIEW
    requestedDecision: APPROVE
    actor: claims.agent
    roles:
      - CLAIMS_AGENT
    documents:
      - type: DAMAGE_PHOTO
        verified: true
    fraudScore: 91
    coverage:
      covered: true
      deductibleAmount: 0.00
      maxPayableAmount: 10000.00
    expectedLegacyStatus: MANUAL_FRAUD_REVIEW
    expectedPaymentPrepared: false
    expectedAuditEvents:
      - CLAIM_DECISION_STARTED
      - FRAUD_CHECKED
      - CLAIM_SENT_TO_FRAUD_REVIEW

  - id: CLAIM_WAIT_FOR_DOCUMENTS
    claimId: CLM-10003
    customerId: CUST-4713
    policyNumber: POL-2024-0003
    currentStatus: WAITING_FOR_DOCUMENTS
    requestedDecision: APPROVE
    actor: claims.manager
    roles:
      - CLAIMS_MANAGER
    documents:
      - type: DAMAGE_PHOTO
        verified: false
    fraudScore: 20
    coverage:
      covered: true
      deductibleAmount: 100.00
      maxPayableAmount: 2000.00
    expectedLegacyStatus: WAITING_FOR_DOCUMENTS
    expectedPaymentPrepared: false
    expectedAuditEvents:
      - CLAIM_DECISION_STARTED
      - DOCUMENTS_MISSING
      - CUSTOMER_NOTIFICATION_REQUESTED

Legacy Characterization Test

Datei:

migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyProcessClaimDecisionCharacterizationTest.java
package com.seb4u.demo.claims.migration;

import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import com.seb4u.demo.claims.legacy.support.FakeLegacyAuditDao;
import com.seb4u.demo.claims.legacy.support.FakeLegacyClaimDao;
import com.seb4u.demo.claims.legacy.support.FakeLegacyDocumentShareClient;
import com.seb4u.demo.claims.legacy.support.FakeLegacyEventPublisher;
import com.seb4u.demo.claims.legacy.support.FakeLegacyPaymentSoapClient;
import com.seb4u.demo.claims.legacy.support.FakePolicyCoverageSoapClient;
import com.seb4u.demo.claims.legacy.support.FakeFraudRiskSoapClient;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class LegacyProcessClaimDecisionCharacterizationTest {

    @Test
    void approveSimpleClaim_keepsCurrentLegacyBehaviourVisible() throws Exception {
        FakeLegacyClaimDao claimDao = new FakeLegacyClaimDao();
        FakeLegacyAuditDao auditDao = new FakeLegacyAuditDao();
        FakeLegacyEventPublisher eventPublisher = new FakeLegacyEventPublisher();

        claimDao.givenClaim("CLM-10001")
            .withCustomerId("CUST-4711")
            .withPolicyNumber("POL-2024-0001")
            .withStatus("PENDING_REVIEW")
            .withRequestedAmount(new BigDecimal("1200.00"));

        LegacyClaimFacadeBean facade = new LegacyClaimFacadeBean(
            claimDao,
            auditDao,
            new FakeLegacyDocumentShareClient()
                .verified("CLM-10001", "DAMAGE_PHOTO")
                .verified("CLM-10001", "INVOICE"),
            new FakeFraudRiskSoapClient().withScore("CLM-10001", 12),
            new FakePolicyCoverageSoapClient()
                .covered("POL-2024-0001", new BigDecimal("250.00"), new BigDecimal("5000.00")),
            new FakeLegacyPaymentSoapClient().paymentPrepared("CLM-10001"),
            eventPublisher
        );

        String resultStatus = facade.processClaimDecision(
            "CLM-10001",
            "APPROVE",
            "claims.manager",
            List.of("CLAIMS_MANAGER"),
            "approved after document review",
            false
        );

        assertThat(resultStatus).isEqualTo("PAYMENT_PENDING");
        assertThat(claimDao.statusOf("CLM-10001")).isEqualTo("PAYMENT_PENDING");
        assertThat(auditDao.eventsFor("CLM-10001"))
            .containsExactly(
                "CLAIM_DECISION_STARTED",
                "COVERAGE_CHECKED",
                "FRAUD_CHECKED",
                "DOCUMENTS_VERIFIED",
                "PAYMENT_PREPARED",
                "CLAIM_APPROVED"
            );
        assertThat(eventPublisher.events())
            .extracting("type")
            .contains("CLAIM_APPROVED", "PAYMENT_PREPARED");
    }
}

Warum ist dieser Test absichtlich noch nicht schön?

Der Test kennt noch Legacy-Namen und Legacy-Statusstrings. Das ist am Anfang okay. Er dient als Schutznetz beim Schneiden der Monster Method.

Wichtig ist:

Golden Master Snapshot

Datei:

migration-tests/src/test/java/com/seb4u/demo/claims/migration/GoldenMasterSnapshot.java
package com.seb4u.demo.claims.migration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class GoldenMasterSnapshot {

    private final String caseId;
    private final String claimId;
    private final String finalStatus;
    private final List<String> auditEvents;
    private final List<String> outboundEvents;
    private final List<String> storedProcedureCalls;
    private final List<String> externalSoapCalls;

    private GoldenMasterSnapshot(Builder builder) {
        this.caseId = builder.caseId;
        this.claimId = builder.claimId;
        this.finalStatus = builder.finalStatus;
        this.auditEvents = List.copyOf(builder.auditEvents);
        this.outboundEvents = List.copyOf(builder.outboundEvents);
        this.storedProcedureCalls = List.copyOf(builder.storedProcedureCalls);
        this.externalSoapCalls = List.copyOf(builder.externalSoapCalls);
    }

    public String caseId() {
        return caseId;
    }

    public String claimId() {
        return claimId;
    }

    public String finalStatus() {
        return finalStatus;
    }

    public List<String> auditEvents() {
        return Collections.unmodifiableList(auditEvents);
    }

    public List<String> outboundEvents() {
        return Collections.unmodifiableList(outboundEvents);
    }

    public List<String> storedProcedureCalls() {
        return Collections.unmodifiableList(storedProcedureCalls);
    }

    public List<String> externalSoapCalls() {
        return Collections.unmodifiableList(externalSoapCalls);
    }

    public static Builder builder(String caseId, String claimId) {
        return new Builder(caseId, claimId);
    }

    public static final class Builder {
        private final String caseId;
        private final String claimId;
        private String finalStatus;
        private final List<String> auditEvents = new ArrayList<>();
        private final List<String> outboundEvents = new ArrayList<>();
        private final List<String> storedProcedureCalls = new ArrayList<>();
        private final List<String> externalSoapCalls = new ArrayList<>();

        private Builder(String caseId, String claimId) {
            this.caseId = caseId;
            this.claimId = claimId;
        }

        public Builder finalStatus(String finalStatus) {
            this.finalStatus = finalStatus;
            return this;
        }

        public Builder auditEvent(String auditEvent) {
            this.auditEvents.add(auditEvent);
            return this;
        }

        public Builder outboundEvent(String outboundEvent) {
            this.outboundEvents.add(outboundEvent);
            return this;
        }

        public Builder storedProcedureCall(String storedProcedureCall) {
            this.storedProcedureCalls.add(storedProcedureCall);
            return this;
        }

        public Builder externalSoapCall(String externalSoapCall) {
            this.externalSoapCalls.add(externalSoapCall);
            return this;
        }

        public GoldenMasterSnapshot build() {
            return new GoldenMasterSnapshot(this);
        }
    }
}

Was ist daran wichtig?

Ein Golden Master Snapshot speichert nicht jedes technische Detail. Er speichert die fachlich und migrationsrelevant wichtigen Beobachtungen:

Damit lÀsst sich spÀter vergleichen:

Reagiert der neue modulare Handler fachlich gleich, obwohl die interne Struktur komplett anders ist?

Teil B: Erste Schnitte an der Monster Method

Schritt 02: Refactoring Seam einfĂŒhren

Was war vorher schlecht?

Die Legacy-Methode hat viele Parameter, direkte Infrastrukturzugriffe und keinen klaren Einstiegspunkt fĂŒr Tests.

Typischer Legacy-Ausschnitt:

public String processClaimDecision(
        String claimId,
        String decision,
        String actorUserId,
        List<String> actorRoles,
        String comment,
        boolean forceApproval) throws Exception {

    // 800+ Zeilen Fachlogik, technische Logik und Nebenwirkungen
}

Was wird geÀndert?

Wir Ă€ndern zunĂ€chst nicht den fachlichen Ablauf. Wir fĂŒhren nur eine interne Methode ein, die spĂ€ter herausgezogen werden kann.

Datei:

before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyClaimFacadeBean.java
package com.seb4u.demo.claims.legacy.ejb;

import java.util.List;

public class LegacyClaimFacadeBean {

    public String processClaimDecision(
            String claimId,
            String decision,
            String actorUserId,
            List<String> actorRoles,
            String comment,
            boolean forceApproval) throws Exception {

        LegacyDecisionInput input = LegacyDecisionInput.from(
            claimId,
            decision,
            actorUserId,
            actorRoles,
            comment,
            forceApproval
        );

        return processClaimDecisionInternal(input);
    }

    String processClaimDecisionInternal(LegacyDecisionInput input) throws Exception {
        // In Schritt 02 enthÀlt diese Methode noch fast den gesamten alten Code.
        // Der Unterschied: Der Ablauf hat jetzt einen testbaren Eingabecontainer.
        // Weitere Schnitte können nun kleiner und kontrollierter durchgefĂŒhrt werden.
        return legacyProcessClaimDecisionStillMostlyUnchanged(input);
    }

    private String legacyProcessClaimDecisionStillMostlyUnchanged(LegacyDecisionInput input) throws Exception {
        // Platzhalter fĂŒr die bestehende Monster Method aus Runde 2.
        // In der echten Codebasis wĂŒrde der bestehende Code hierher verschoben,
        // ohne die Logik fachlich zu Àndern.
        throw new UnsupportedOperationException("legacy body moved here during refactoring seam step");
    }
}

Datei:

before_refactoring_legacy/legacy-claims-ejb/src/main/java/com/seb4u/demo/claims/legacy/ejb/LegacyDecisionInput.java
package com.seb4u.demo.claims.legacy.ejb;

import java.util.List;
import java.util.Objects;

final class LegacyDecisionInput {

    private final String claimId;
    private final String decision;
    private final String actorUserId;
    private final List<String> actorRoles;
    private final String comment;
    private final boolean forceApproval;

    private LegacyDecisionInput(
            String claimId,
            String decision,
            String actorUserId,
            List<String> actorRoles,
            String comment,
            boolean forceApproval) {
        this.claimId = requireText(claimId, "claimId");
        this.decision = requireText(decision, "decision");
        this.actorUserId = requireText(actorUserId, "actorUserId");
        this.actorRoles = List.copyOf(Objects.requireNonNull(actorRoles, "actorRoles"));
        this.comment = comment == null ? "" : comment.trim();
        this.forceApproval = forceApproval;
    }

    static LegacyDecisionInput from(
            String claimId,
            String decision,
            String actorUserId,
            List<String> actorRoles,
            String comment,
            boolean forceApproval) {
        return new LegacyDecisionInput(claimId, decision, actorUserId, actorRoles, comment, forceApproval);
    }

    String claimId() {
        return claimId;
    }

    String decision() {
        return decision;
    }

    String actorUserId() {
        return actorUserId;
    }

    List<String> actorRoles() {
        return actorRoles;
    }

    String comment() {
        return comment;
    }

    boolean forceApproval() {
        return forceApproval;
    }

    private static String requireText(String value, String fieldName) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + " must not be blank");
        }
        return value.trim();
    }
}

Warum wurde es geÀndert?

Damit die ursprĂŒngliche Signatur stabil bleibt, aber intern ein kontrollierter Einstiegspunkt entsteht.

Pattern / Konzept

Tests

Der vorhandene Characterization Test muss weiterhin grĂŒn bleiben.

Risiko

Schon das Verschieben von Code kann Seiteneffekte verÀndern, wenn Initialisierung, Transaktionen oder Lazy Loading betroffen sind. Deshalb darf in diesem Schritt keine fachliche Logik verÀndert werden.

Schritt 03: Command Object extrahieren

Was war vorher schlecht?

Die Parameterliste reprÀsentiert eigentlich einen fachlichen Auftrag. Im Legacy-Code bleibt das unsichtbar.

Was wird geÀndert?

Aus LegacyDecisionInput wird spÀter ein modernes ProcessClaimDecisionCommand.

Datei:

after_refactoring_modular/claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionCommand.java
package com.seb4u.demo.claims.application.decision;

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.DecisionComment;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.ActorId;
import com.seb4u.demo.claims.shared.security.ActorRole;

import java.time.Instant;
import java.util.Set;

public final class ProcessClaimDecisionCommand {

    private final ClaimId claimId;
    private final RequestedDecision requestedDecision;
    private final ActorId actorId;
    private final Set<ActorRole> actorRoles;
    private final DecisionComment comment;
    private final boolean forceApproval;
    private final Instant requestedAt;
    private final String correlationId;

    private ProcessClaimDecisionCommand(Builder builder) {
        this.claimId = builder.claimId;
        this.requestedDecision = builder.requestedDecision;
        this.actorId = builder.actorId;
        this.actorRoles = Set.copyOf(builder.actorRoles);
        this.comment = builder.comment;
        this.forceApproval = builder.forceApproval;
        this.requestedAt = builder.requestedAt;
        this.correlationId = builder.correlationId;
    }

    public ClaimId claimId() {
        return claimId;
    }

    public RequestedDecision requestedDecision() {
        return requestedDecision;
    }

    public ActorId actorId() {
        return actorId;
    }

    public Set<ActorRole> actorRoles() {
        return actorRoles;
    }

    public DecisionComment comment() {
        return comment;
    }

    public boolean forceApproval() {
        return forceApproval;
    }

    public Instant requestedAt() {
        return requestedAt;
    }

    public String correlationId() {
        return correlationId;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private ClaimId claimId;
        private RequestedDecision requestedDecision;
        private ActorId actorId;
        private Set<ActorRole> actorRoles = Set.of();
        private DecisionComment comment = DecisionComment.empty();
        private boolean forceApproval;
        private Instant requestedAt = Instant.now();
        private String correlationId;

        public Builder claimId(ClaimId claimId) {
            this.claimId = claimId;
            return this;
        }

        public Builder requestedDecision(RequestedDecision requestedDecision) {
            this.requestedDecision = requestedDecision;
            return this;
        }

        public Builder actorId(ActorId actorId) {
            this.actorId = actorId;
            return this;
        }

        public Builder actorRoles(Set<ActorRole> actorRoles) {
            this.actorRoles = actorRoles;
            return this;
        }

        public Builder comment(DecisionComment comment) {
            this.comment = comment;
            return this;
        }

        public Builder forceApproval(boolean forceApproval) {
            this.forceApproval = forceApproval;
            return this;
        }

        public Builder requestedAt(Instant requestedAt) {
            this.requestedAt = requestedAt;
            return this;
        }

        public Builder correlationId(String correlationId) {
            this.correlationId = correlationId;
            return this;
        }

        public ProcessClaimDecisionCommand build() {
            if (claimId == null) {
                throw new IllegalStateException("claimId is required");
            }
            if (requestedDecision == null) {
                throw new IllegalStateException("requestedDecision is required");
            }
            if (actorId == null) {
                throw new IllegalStateException("actorId is required");
            }
            if (correlationId == null || correlationId.isBlank()) {
                throw new IllegalStateException("correlationId is required");
            }
            return new ProcessClaimDecisionCommand(this);
        }
    }
}

Warum wurde es geÀndert?

Der Use Case bekommt einen fachlichen Namen. SpÀter kann derselbe Command aus SOAP, REST, Batch oder Backoffice kommen.

Pattern / Konzept

Tests

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:

Was wird geÀndert?

Ein fachliches Ergebnisobjekt wird eingefĂŒhrt.

Datei:

claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionResult.java
package com.seb4u.demo.claims.application.decision;

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.DecisionReason;

import java.util.ArrayList;
import java.util.List;

public final class ProcessClaimDecisionResult {

    private final ClaimId claimId;
    private final ClaimStatus previousStatus;
    private final ClaimStatus newStatus;
    private final DecisionReason reason;
    private final boolean paymentPreparationRequired;
    private final boolean customerNotificationRequired;
    private final boolean managerEscalationRequired;
    private final List<String> emittedDomainEvents;

    private ProcessClaimDecisionResult(Builder builder) {
        this.claimId = builder.claimId;
        this.previousStatus = builder.previousStatus;
        this.newStatus = builder.newStatus;
        this.reason = builder.reason;
        this.paymentPreparationRequired = builder.paymentPreparationRequired;
        this.customerNotificationRequired = builder.customerNotificationRequired;
        this.managerEscalationRequired = builder.managerEscalationRequired;
        this.emittedDomainEvents = List.copyOf(builder.emittedDomainEvents);
    }

    public ClaimId claimId() {
        return claimId;
    }

    public ClaimStatus previousStatus() {
        return previousStatus;
    }

    public ClaimStatus newStatus() {
        return newStatus;
    }

    public DecisionReason reason() {
        return reason;
    }

    public boolean paymentPreparationRequired() {
        return paymentPreparationRequired;
    }

    public boolean customerNotificationRequired() {
        return customerNotificationRequired;
    }

    public boolean managerEscalationRequired() {
        return managerEscalationRequired;
    }

    public List<String> emittedDomainEvents() {
        return emittedDomainEvents;
    }

    public static Builder builder(ClaimId claimId) {
        return new Builder(claimId);
    }

    public static final class Builder {
        private final ClaimId claimId;
        private ClaimStatus previousStatus;
        private ClaimStatus newStatus;
        private DecisionReason reason;
        private boolean paymentPreparationRequired;
        private boolean customerNotificationRequired;
        private boolean managerEscalationRequired;
        private final List<String> emittedDomainEvents = new ArrayList<>();

        private Builder(ClaimId claimId) {
            this.claimId = claimId;
        }

        public Builder previousStatus(ClaimStatus previousStatus) {
            this.previousStatus = previousStatus;
            return this;
        }

        public Builder newStatus(ClaimStatus newStatus) {
            this.newStatus = newStatus;
            return this;
        }

        public Builder reason(DecisionReason reason) {
            this.reason = reason;
            return this;
        }

        public Builder paymentPreparationRequired(boolean value) {
            this.paymentPreparationRequired = value;
            return this;
        }

        public Builder customerNotificationRequired(boolean value) {
            this.customerNotificationRequired = value;
            return this;
        }

        public Builder managerEscalationRequired(boolean value) {
            this.managerEscalationRequired = value;
            return this;
        }

        public Builder emit(String eventType) {
            this.emittedDomainEvents.add(eventType);
            return this;
        }

        public ProcessClaimDecisionResult build() {
            if (previousStatus == null || newStatus == null || reason == null) {
                throw new IllegalStateException("previousStatus, newStatus and reason are required");
            }
            return new ProcessClaimDecisionResult(this);
        }
    }
}

Warum wurde es geÀndert?

Das Ergebnis wird fachlich erklÀrbar. SOAP kann daraus weiterhin einen String oder eine alte Response erzeugen. REST kann daraus eine moderne JSON-Antwort erzeugen.

Pattern / Konzept

Tests

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:

Dadurch entstehen Fehler wie:

Was wird geÀndert?

Wichtige Fachbegriffe werden als Value Objects modelliert.

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/ClaimId.java
package com.seb4u.demo.claims.domain.model;

import java.util.Objects;
import java.util.regex.Pattern;

public final class ClaimId {

    private static final Pattern FORMAT = Pattern.compile("CLM-[0-9]{5,12}");

    private final String value;

    private ClaimId(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("claimId must not be blank");
        }
        String normalized = value.trim().toUpperCase();
        if (!FORMAT.matcher(normalized).matches()) {
            throw new IllegalArgumentException("invalid claimId format: " + value);
        }
        this.value = normalized;
    }

    public static ClaimId of(String value) {
        return new ClaimId(value);
    }

    public String value() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ClaimId claimId)) return false;
        return value.equals(claimId.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return value;
    }
}

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/RequestedDecision.java
package com.seb4u.demo.claims.domain.model;

public enum RequestedDecision {
    APPROVE,
    REJECT,
    ESCALATE,
    REQUEST_MORE_DOCUMENTS,
    SEND_TO_FRAUD_REVIEW;

    public static RequestedDecision fromLegacyCode(String code) {
        if (code == null || code.isBlank()) {
            throw new IllegalArgumentException("decision must not be blank");
        }
        return switch (code.trim().toUpperCase()) {
            case "APPROVE", "APPROVED" -> APPROVE;
            case "REJECT", "REJECTED" -> REJECT;
            case "ESCALATE", "ESCALATED" -> ESCALATE;
            case "REQ_DOC", "REQUEST_MORE_DOCUMENTS" -> REQUEST_MORE_DOCUMENTS;
            case "FRAUD_REVIEW", "SEND_TO_FRAUD_REVIEW" -> SEND_TO_FRAUD_REVIEW;
            default -> throw new IllegalArgumentException("unsupported decision: " + code);
        };
    }
}

Pattern / Konzept

Tests

Datei:

claim-domain/src/test/java/com/seb4u/demo/claims/domain/model/RequestedDecisionTest.java
package com.seb4u.demo.claims.domain.model;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class RequestedDecisionTest {

    @Test
    void mapsLegacyApproveCodes() {
        assertThat(RequestedDecision.fromLegacyCode("APPROVE"))
            .isEqualTo(RequestedDecision.APPROVE);
        assertThat(RequestedDecision.fromLegacyCode("APPROVED"))
            .isEqualTo(RequestedDecision.APPROVE);
    }

    @Test
    void rejectsUnknownLegacyCode() {
        assertThatThrownBy(() -> RequestedDecision.fromLegacyCode("PAY_NOW_AND_IGNORE_FRAUD"))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("unsupported decision");
    }
}

Schritt 06: Statuslogik in Policy ĂŒberfĂŒhren

Was war vorher schlecht?

Im Legacy-Code steht Statuslogik verteilt:

if ("PENDING_REVIEW".equals(status) && "APPROVE".equals(decision)) {
    if (fraudScore < 80 && coverageCovered && allDocsVerified) {
        status = "PAYMENT_PENDING";
    } else if (fraudScore >= 80) {
        status = "MANUAL_FRAUD_REVIEW";
    } else if (!coverageCovered) {
        status = "REJECTED";
    } else {
        status = "WAITING_FOR_DOCUMENTS";
    }
} else if ("WAITING_FOR_DOCUMENTS".equals(status) && "APPROVE".equals(decision)) {
    // weitere SonderfÀlle
}

Das Problem:

Was wird geÀndert?

StatusĂŒbergĂ€nge werden in ClaimTransitionPolicy gekapselt.

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicy.java
package com.seb4u.demo.claims.domain.workflow;

import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.DecisionReason;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.model.TransitionDecision;

public final class ClaimTransitionPolicy {

    public TransitionDecision decide(ClaimTransitionContext context) {
        if (context.currentStatus() == ClaimStatus.CLOSED) {
            return TransitionDecision.rejectTransition(
                context.currentStatus(),
                DecisionReason.of("closed claims cannot be changed")
            );
        }

        if (context.requestedDecision() == RequestedDecision.REQUEST_MORE_DOCUMENTS) {
            return TransitionDecision.moveTo(
                ClaimStatus.WAITING_FOR_DOCUMENTS,
                DecisionReason.of("additional documents requested")
            );
        }

        if (!context.authorization().mayProcessDecision()) {
            return TransitionDecision.rejectTransition(
                context.currentStatus(),
                DecisionReason.of("actor is not allowed to process claim decision")
            );
        }

        if (!context.documents().allRequiredDocumentsVerified()) {
            return TransitionDecision.moveTo(
                ClaimStatus.WAITING_FOR_DOCUMENTS,
                DecisionReason.of("required documents are missing or not verified")
            );
        }

        if (context.fraudRisk().requiresManualReview()) {
            return TransitionDecision.moveTo(
                ClaimStatus.MANUAL_FRAUD_REVIEW,
                DecisionReason.of("fraud score requires manual review")
            );
        }

        if (!context.coverage().covered()) {
            return TransitionDecision.moveTo(
                ClaimStatus.REJECTED,
                DecisionReason.of("claim is not covered by policy")
            );
        }

        return switch (context.requestedDecision()) {
            case APPROVE -> TransitionDecision.moveTo(
                ClaimStatus.PAYMENT_PENDING,
                DecisionReason.of("claim approved and payment preparation required")
            );
            case REJECT -> TransitionDecision.moveTo(
                ClaimStatus.REJECTED,
                DecisionReason.of("claim rejected by authorized actor")
            );
            case ESCALATE -> TransitionDecision.moveTo(
                ClaimStatus.ESCALATED,
                DecisionReason.of("claim escalated by actor")
            );
            case SEND_TO_FRAUD_REVIEW -> TransitionDecision.moveTo(
                ClaimStatus.MANUAL_FRAUD_REVIEW,
                DecisionReason.of("manual fraud review requested")
            );
            case REQUEST_MORE_DOCUMENTS -> TransitionDecision.moveTo(
                ClaimStatus.WAITING_FOR_DOCUMENTS,
                DecisionReason.of("additional documents requested")
            );
        };
    }
}

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionContext.java
package com.seb4u.demo.claims.domain.workflow;

import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;

public record ClaimTransitionContext(
    ClaimStatus currentStatus,
    RequestedDecision requestedDecision,
    AuthorizationDecision authorization,
    DocumentVerificationSummary documents,
    FraudRiskAssessment fraudRisk,
    CoverageDecision coverage
) {
}

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/model/TransitionDecision.java
package com.seb4u.demo.claims.domain.model;

public final class TransitionDecision {

    private final boolean allowed;
    private final ClaimStatus targetStatus;
    private final DecisionReason reason;

    private TransitionDecision(boolean allowed, ClaimStatus targetStatus, DecisionReason reason) {
        this.allowed = allowed;
        this.targetStatus = targetStatus;
        this.reason = reason;
    }

    public static TransitionDecision moveTo(ClaimStatus targetStatus, DecisionReason reason) {
        return new TransitionDecision(true, targetStatus, reason);
    }

    public static TransitionDecision rejectTransition(ClaimStatus currentStatus, DecisionReason reason) {
        return new TransitionDecision(false, currentStatus, reason);
    }

    public boolean allowed() {
        return allowed;
    }

    public ClaimStatus targetStatus() {
        return targetStatus;
    }

    public DecisionReason reason() {
        return reason;
    }
}

Tests

Datei:

claim-domain/src/test/java/com/seb4u/demo/claims/domain/workflow/ClaimTransitionPolicyTest.java
package com.seb4u.demo.claims.domain.workflow;

import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.model.TransitionDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ClaimTransitionPolicyTest {

    private final ClaimTransitionPolicy policy = new ClaimTransitionPolicy();

    @Test
    void movesApprovedCoveredLowRiskClaimToPaymentPending() {
        ClaimTransitionContext context = new ClaimTransitionContext(
            ClaimStatus.PENDING_REVIEW,
            RequestedDecision.APPROVE,
            AuthorizationDecision.allowed("manager may approve"),
            DocumentVerificationSummary.allRequiredVerified(),
            FraudRiskAssessment.lowRisk(12),
            CoverageDecision.covered()
        );

        TransitionDecision decision = policy.decide(context);

        assertThat(decision.allowed()).isTrue();
        assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.PAYMENT_PENDING);
        assertThat(decision.reason().value()).contains("payment preparation required");
    }

    @Test
    void sendsHighRiskClaimToManualFraudReviewBeforePayment() {
        ClaimTransitionContext context = new ClaimTransitionContext(
            ClaimStatus.PENDING_REVIEW,
            RequestedDecision.APPROVE,
            AuthorizationDecision.allowed("manager may approve"),
            DocumentVerificationSummary.allRequiredVerified(),
            FraudRiskAssessment.highRisk(91),
            CoverageDecision.covered()
        );

        TransitionDecision decision = policy.decide(context);

        assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.MANUAL_FRAUD_REVIEW);
        assertThat(decision.reason().value()).contains("fraud score");
    }

    @Test
    void waitsForDocumentsWhenRequiredDocumentsAreMissing() {
        ClaimTransitionContext context = new ClaimTransitionContext(
            ClaimStatus.PENDING_REVIEW,
            RequestedDecision.APPROVE,
            AuthorizationDecision.allowed("manager may approve"),
            DocumentVerificationSummary.missingRequired("INVOICE"),
            FraudRiskAssessment.lowRisk(10),
            CoverageDecision.covered()
        );

        TransitionDecision decision = policy.decide(context);

        assertThat(decision.targetStatus()).isEqualTo(ClaimStatus.WAITING_FOR_DOCUMENTS);
    }
}

Warum wurde es geÀndert?

Jetzt kann die Statuslogik ohne WebSphere, SOAP, Oracle, JMS oder File Share getestet werden.

Pattern / Konzept

Risiko

Die Reihenfolge der alten if/else-Regeln kann fachlich relevant gewesen sein. Deshalb muss jeder neue Policy-Test gegen Golden-Master-Szenarien geprĂŒft werden.

Teil D: Ports & Adapters extrahieren

Schritt 07: Document Verification als Port

Was war vorher schlecht?

Dokumente wurden direkt in der EJB aus File Shares und SOAP-Metadaten geprĂŒft.

Typische Legacy-Probleme:

Was wird geÀndert?

Die Anwendung kennt nur noch Ports.

Datei:

claim-document/src/main/java/com/seb4u/demo/claims/document/DocumentVerificationPort.java
package com.seb4u.demo.claims.document;

import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;

public interface DocumentVerificationPort {

    DocumentVerificationSummary verifyRequiredDocuments(ClaimId claimId);
}

Datei:

claim-document/src/main/java/com/seb4u/demo/claims/document/FileShareDocumentVerificationAdapter.java
package com.seb4u.demo.claims.document;

import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public final class FileShareDocumentVerificationAdapter implements DocumentVerificationPort {

    private final Path rootDirectory;
    private final LegacyDocumentMetadataReader metadataReader;

    public FileShareDocumentVerificationAdapter(Path rootDirectory, LegacyDocumentMetadataReader metadataReader) {
        this.rootDirectory = rootDirectory;
        this.metadataReader = metadataReader;
    }

    @Override
    public DocumentVerificationSummary verifyRequiredDocuments(ClaimId claimId) {
        Path claimFolder = rootDirectory.resolve(claimId.value());
        if (!Files.exists(claimFolder)) {
            return DocumentVerificationSummary.missingRequired("CLAIM_FOLDER");
        }

        List<String> missing = new ArrayList<>();
        verifyDocument(claimFolder, "DAMAGE_PHOTO", missing);
        verifyDocument(claimFolder, "INVOICE", missing);

        if (!missing.isEmpty()) {
            return DocumentVerificationSummary.missingRequired(missing);
        }

        LegacyDocumentMetadata metadata = metadataReader.readMetadata(claimFolder);
        if (!metadata.allDocumentsVirusChecked()) {
            return DocumentVerificationSummary.notVerified("virus scan missing");
        }

        if (!metadata.allDocumentsAssignedToClaim(claimId.value())) {
            return DocumentVerificationSummary.notVerified("metadata claim assignment mismatch");
        }

        return DocumentVerificationSummary.allRequiredVerified();
    }

    private void verifyDocument(Path claimFolder, String documentType, List<String> missing) {
        Path file = claimFolder.resolve(documentType + ".pdf");
        if (!Files.exists(file)) {
            missing.add(documentType);
        }
    }
}

Pattern / Konzept

Tests

package com.seb4u.demo.claims.document;

import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.model.ClaimId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;

import static org.assertj.core.api.Assertions.assertThat;

class FileShareDocumentVerificationAdapterTest {

    @TempDir
    Path tempDir;

    @Test
    void returnsMissingDocumentsWhenInvoiceIsAbsent() throws Exception {
        ClaimId claimId = ClaimId.of("CLM-12345");
        Path claimFolder = Files.createDirectories(tempDir.resolve(claimId.value()));
        Files.writeString(claimFolder.resolve("DAMAGE_PHOTO.pdf"), "fake-pdf");

        FileShareDocumentVerificationAdapter adapter = new FileShareDocumentVerificationAdapter(
            tempDir,
            folder -> LegacyDocumentMetadata.validFor(claimId.value())
        );

        DocumentVerificationSummary summary = adapter.verifyRequiredDocuments(claimId);

        assertThat(summary.allRequiredDocumentsVerified()).isFalse();
        assertThat(summary.missingDocumentTypes()).contains("INVOICE");
    }
}

Risiko

File-System-Adapter können auf OpenShift problematisch sein, wenn sie lokale Pfade erwarten. Deshalb wird der Port so geschnitten, dass spÀter Object Storage oder ein DMS-Service eingebunden werden kann.

Schritt 08: Fraud Risk als Port

Datei:

claim-application/src/main/java/com/seb4u/demo/claims/application/ports/FraudRiskPort.java
package com.seb4u.demo.claims.application.ports;

import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;

public interface FraudRiskPort {

    FraudRiskAssessment assessRisk(ClaimId claimId, CustomerSnapshot customerSnapshot);
}

Datei:

claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/fraud/FraudRiskSoapAdapter.java
package com.seb4u.demo.claims.infrastructure.fraud;

import com.seb4u.demo.claims.application.ports.FraudRiskPort;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.ClaimId;

public final class FraudRiskSoapAdapter implements FraudRiskPort {

    private final FraudRiskSoapClient client;
    private final FraudRiskMapper mapper;
    private final FraudRiskTimeoutPolicy timeoutPolicy;

    public FraudRiskSoapAdapter(
            FraudRiskSoapClient client,
            FraudRiskMapper mapper,
            FraudRiskTimeoutPolicy timeoutPolicy) {
        this.client = client;
        this.mapper = mapper;
        this.timeoutPolicy = timeoutPolicy;
    }

    @Override
    public FraudRiskAssessment assessRisk(ClaimId claimId, CustomerSnapshot customerSnapshot) {
        FraudRiskRequest request = mapper.toRequest(claimId, customerSnapshot);

        try {
            FraudRiskResponse response = timeoutPolicy.execute(
                () -> client.calculateRisk(request),
                "fraud-risk",
                claimId.value()
            );
            return mapper.toDomain(response);
        } catch (FraudRiskTimeoutException timeout) {
            return FraudRiskAssessment.unknownRequiresManualReview(
                "fraud risk timeout: " + timeout.getMessage()
            );
        } catch (FraudRiskTechnicalException technical) {
            return FraudRiskAssessment.unknownRequiresManualReview(
                "fraud risk technical error: " + technical.getMessage()
            );
        }
    }
}

Was wurde verbessert?

Pattern / Konzept

Risiko

Nicht jeder technische Fehler darf fachlich gleich behandelt werden. Deshalb muss mit Fachbereich und Betrieb geklÀrt werden:

Schritt 09: Coverage als Specification/Policy

Was war vorher schlecht?

Coverage wurde direkt ĂŒber SOAP/Stored Procedure geprĂŒft. Die alte Tariflogik war schwer lesbar.

Was wird geÀndert?

Technische Coverage-Ermittlung und fachliche Coverage-Entscheidung werden getrennt.

Datei:

claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CoveragePort.java
package com.seb4u.demo.claims.application.ports;

import com.seb4u.demo.claims.domain.coverage.CoverageSnapshot;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.policy.PolicyNumber;

public interface CoveragePort {

    CoverageSnapshot loadCoverage(ClaimId claimId, PolicyNumber policyNumber);
}

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/coverage/CoveragePolicy.java
package com.seb4u.demo.claims.domain.coverage;

import com.seb4u.demo.claims.domain.model.ClaimAmount;

public final class CoveragePolicy {

    public CoverageDecision decide(CoverageSnapshot snapshot, ClaimAmount requestedAmount) {
        if (!snapshot.policyActive()) {
            return CoverageDecision.notCovered("policy is not active");
        }

        if (snapshot.exclusionCodes().contains("INTENTIONAL_DAMAGE")) {
            return CoverageDecision.notCovered("intentional damage exclusion applies");
        }

        if (requestedAmount.isGreaterThan(snapshot.maxPayableAmount())) {
            return CoverageDecision.partiallyCovered(
                snapshot.maxPayableAmount(),
                "requested amount exceeds maximum payable amount"
            );
        }

        return CoverageDecision.covered(snapshot.deductibleAmount(), requestedAmount);
    }
}

Pattern / Konzept

Test

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

import com.seb4u.demo.claims.domain.model.ClaimAmount;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class CoveragePolicyTest {

    @Test
    void rejectsClaimWhenPolicyIsInactive() {
        CoverageSnapshot snapshot = new CoverageSnapshot(
            false,
            new BigDecimal("0.00"),
            new BigDecimal("5000.00"),
            Set.of()
        );

        CoverageDecision decision = new CoveragePolicy()
            .decide(snapshot, ClaimAmount.of("1200.00"));

        assertThat(decision.covered()).isFalse();
        assertThat(decision.reason()).contains("not active");
    }
}

Schritt 10: Payment Release als Port plus Idempotency

Was war vorher schlecht?

Im Legacy-Code wurde Payment innerhalb der gleichen JTA-Transaktion wie Claim-Status, Audit und JMS vorbereitet. Das ist gefÀhrlich:

Was wird geÀndert?

Payment wird ĂŒber einen Port angesprochen und mit Idempotency Key abgesichert.

Datei:

claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentReleasePort.java
package com.seb4u.demo.claims.payment;

import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.payment.PaymentPreparationResult;

public interface PaymentReleasePort {

    PaymentPreparationResult preparePayment(PaymentPreparationRequest request);

    PaymentPreparationResult findByIdempotencyKey(IdempotencyKey idempotencyKey);
}

Datei:

claim-payment/src/main/java/com/seb4u/demo/claims/payment/PaymentPreparationRequest.java
package com.seb4u.demo.claims.payment;

import com.seb4u.demo.claims.domain.model.ClaimAmount;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.customer.CustomerId;

public record PaymentPreparationRequest(
    ClaimId claimId,
    CustomerId customerId,
    ClaimAmount amount,
    IdempotencyKey idempotencyKey,
    String reason
) {
}

Datei:

claim-payment/src/main/java/com/seb4u/demo/claims/payment/IdempotencyKey.java
package com.seb4u.demo.claims.payment;

import com.seb4u.demo.claims.domain.model.ClaimId;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public final class IdempotencyKey {

    private final String value;

    private IdempotencyKey(String value) {
        this.value = value;
    }

    public static IdempotencyKey forPaymentPreparation(ClaimId claimId, String decisionVersion) {
        return new IdempotencyKey("PAYMENT_PREPARE-" + sha256(claimId.value() + ":" + decisionVersion));
    }

    public String value() {
        return value;
    }

    private static String sha256(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder();
            for (byte b : hash) {
                hex.append(String.format("%02x", b));
            }
            return hex.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 not available", e);
        }
    }
}

Pattern / Konzept

Risiko

Idempotency muss mit dem Zielsystem abgestimmt werden. Wenn das alte Payment-System keinen Idempotency Key unterstĂŒtzt, muss der Adapter lokal eine Idempotency-Tabelle fĂŒhren.

Teil E: Customer und Identity entkoppeln

Schritt 11: Customer Master Data Port

Datei:

claim-application/src/main/java/com/seb4u/demo/claims/application/ports/CustomerMasterDataPort.java
package com.seb4u.demo.claims.application.ports;

import com.seb4u.demo.claims.domain.customer.CustomerId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;

public interface CustomerMasterDataPort {

    CustomerSnapshot loadCustomer(CustomerId customerId);
}

Datei:

claim-infrastructure/src/main/java/com/seb4u/demo/claims/infrastructure/customer/CustomerMasterDataSoapAdapter.java
package com.seb4u.demo.claims.infrastructure.customer;

import com.seb4u.demo.claims.application.ports.CustomerMasterDataPort;
import com.seb4u.demo.claims.domain.customer.CustomerId;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;

public final class CustomerMasterDataSoapAdapter implements CustomerMasterDataPort {

    private final CustomerMasterDataSoapClient client;
    private final CustomerMasterDataMapper mapper;

    public CustomerMasterDataSoapAdapter(
            CustomerMasterDataSoapClient client,
            CustomerMasterDataMapper mapper) {
        this.client = client;
        this.mapper = mapper;
    }

    @Override
    public CustomerSnapshot loadCustomer(CustomerId customerId) {
        CustomerMasterDataResponse response = client.getCustomer(customerId.value());
        return mapper.toSnapshot(response);
    }
}

Warum wurde es geÀndert?

Der Claim-Use-Case braucht eine stabile Momentaufnahme des Kunden, aber keine technische AbhÀngigkeit auf SOAP oder alte Kundennummernlogik.

Schritt 12: SecurityContextPort und AuthorizationPolicy

Was war vorher schlecht?

Rollen wurden im Legacy-Code direkt geprĂŒft:

if (!roles.contains("CLAIMS_MANAGER") && forceApproval) {
    throw new SecurityException("not allowed");
}

Problem:

Was wird geÀndert?

Authentifizierungskontext und fachliche Autorisierung werden getrennt.

Datei:

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

public interface SecurityContextPort {

    AuthenticatedActor currentActor();
}

Datei:

claim-domain/src/main/java/com/seb4u/demo/claims/domain/security/AuthorizationPolicy.java
package com.seb4u.demo.claims.domain.security;

import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.ActorRole;

public final class AuthorizationPolicy {

    public AuthorizationDecision mayProcessDecision(
            AuthenticatedActor actor,
            ClaimStatus currentStatus,
            RequestedDecision requestedDecision,
            boolean forceApproval) {

        if (actor.hasRole(ActorRole.SYSTEM_MIGRATION)) {
            return AuthorizationDecision.allowed("system migration actor");
        }

        if (requestedDecision == RequestedDecision.APPROVE && forceApproval) {
            return actor.hasRole(ActorRole.CLAIMS_MANAGER)
                ? AuthorizationDecision.allowed("manager may force approval")
                : AuthorizationDecision.denied("force approval requires CLAIMS_MANAGER");
        }

        if (requestedDecision == RequestedDecision.REJECT) {
            return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER)
                ? AuthorizationDecision.allowed("claims agent may reject")
                : AuthorizationDecision.denied("reject requires internal claims role");
        }

        if (requestedDecision == RequestedDecision.REQUEST_MORE_DOCUMENTS) {
            return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER, ActorRole.PARTNER_SUPPORT)
                ? AuthorizationDecision.allowed("actor may request documents")
                : AuthorizationDecision.denied("requesting documents not allowed");
        }

        if (currentStatus == ClaimStatus.ESCALATED) {
            return actor.hasRole(ActorRole.CLAIMS_MANAGER)
                ? AuthorizationDecision.allowed("manager may process escalated claim")
                : AuthorizationDecision.denied("escalated claims require manager");
        }

        return actor.hasAnyRole(ActorRole.CLAIMS_AGENT, ActorRole.CLAIMS_MANAGER)
            ? AuthorizationDecision.allowed("standard claims processing")
            : AuthorizationDecision.denied("actor is not allowed to process claim decision");
    }
}

Pattern / Konzept

Tests

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

import com.seb4u.demo.claims.domain.model.ClaimStatus;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.ActorId;
import com.seb4u.demo.claims.shared.security.ActorRole;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class AuthorizationPolicyTest {

    private final AuthorizationPolicy policy = new AuthorizationPolicy();

    @Test
    void deniesForceApprovalForClaimsAgent() {
        AuthenticatedActor actor = new AuthenticatedActor(
            ActorId.of("agent-1"),
            Set.of(ActorRole.CLAIMS_AGENT)
        );

        AuthorizationDecision decision = policy.mayProcessDecision(
            actor,
            ClaimStatus.PENDING_REVIEW,
            RequestedDecision.APPROVE,
            true
        );

        assertThat(decision.allowed()).isFalse();
        assertThat(decision.reason()).contains("CLAIMS_MANAGER");
    }
}

Risiko

Alte WebSphere-Rollen können anders gruppiert sein als moderne Identity-Provider-Rollen. Deshalb braucht die Migration ein eigenes Role-Mapping-Dokument.

Teil F: Workflow Orchestrator

Schritt 13: ProcessClaimDecisionHandler als Application Service

Was war vorher schlecht?

Die EJB war gleichzeitig technischer Entry Point und fachlicher Ablauf.

Was wird geÀndert?

Die eigentliche Use-Case-Logik wird in einen Application Handler verschoben.

Datei:

claim-application/src/main/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandler.java
package com.seb4u.demo.claims.application.decision;

import com.seb4u.demo.claims.application.ports.ClaimRepository;
import com.seb4u.demo.claims.application.ports.CoveragePort;
import com.seb4u.demo.claims.application.ports.CustomerMasterDataPort;
import com.seb4u.demo.claims.application.ports.FraudRiskPort;
import com.seb4u.demo.claims.audit.AuditLogPort;
import com.seb4u.demo.claims.document.DocumentVerificationPort;
import com.seb4u.demo.claims.domain.coverage.CoverageDecision;
import com.seb4u.demo.claims.domain.coverage.CoveragePolicy;
import com.seb4u.demo.claims.domain.coverage.CoverageSnapshot;
import com.seb4u.demo.claims.domain.customer.CustomerSnapshot;
import com.seb4u.demo.claims.domain.document.DocumentVerificationSummary;
import com.seb4u.demo.claims.domain.fraud.FraudRiskAssessment;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.TransitionDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionContext;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionPolicy;
import com.seb4u.demo.claims.payment.PaymentPreparationRequest;
import com.seb4u.demo.claims.payment.PaymentReleasePort;
import com.seb4u.demo.claims.shared.security.AuthenticatedActor;
import com.seb4u.demo.claims.shared.security.SecurityContextPort;

public final class ProcessClaimDecisionHandler {

    private final ClaimRepository claimRepository;
    private final CustomerMasterDataPort customerMasterDataPort;
    private final SecurityContextPort securityContextPort;
    private final AuthorizationPolicy authorizationPolicy;
    private final DocumentVerificationPort documentVerificationPort;
    private final FraudRiskPort fraudRiskPort;
    private final CoveragePort coveragePort;
    private final CoveragePolicy coveragePolicy;
    private final ClaimTransitionPolicy transitionPolicy;
    private final PaymentReleasePort paymentReleasePort;
    private final AuditLogPort auditLogPort;
    private final ClaimDecisionOutbox outbox;

    public ProcessClaimDecisionHandler(
            ClaimRepository claimRepository,
            CustomerMasterDataPort customerMasterDataPort,
            SecurityContextPort securityContextPort,
            AuthorizationPolicy authorizationPolicy,
            DocumentVerificationPort documentVerificationPort,
            FraudRiskPort fraudRiskPort,
            CoveragePort coveragePort,
            CoveragePolicy coveragePolicy,
            ClaimTransitionPolicy transitionPolicy,
            PaymentReleasePort paymentReleasePort,
            AuditLogPort auditLogPort,
            ClaimDecisionOutbox outbox) {
        this.claimRepository = claimRepository;
        this.customerMasterDataPort = customerMasterDataPort;
        this.securityContextPort = securityContextPort;
        this.authorizationPolicy = authorizationPolicy;
        this.documentVerificationPort = documentVerificationPort;
        this.fraudRiskPort = fraudRiskPort;
        this.coveragePort = coveragePort;
        this.coveragePolicy = coveragePolicy;
        this.transitionPolicy = transitionPolicy;
        this.paymentReleasePort = paymentReleasePort;
        this.auditLogPort = auditLogPort;
        this.outbox = outbox;
    }

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        AuthenticatedActor actor = securityContextPort.currentActor();

        Claim claim = claimRepository.getById(command.claimId());
        auditLogPort.append(command.claimId(), "CLAIM_DECISION_STARTED", actor.id().value(), command.correlationId());

        AuthorizationDecision authorization = authorizationPolicy.mayProcessDecision(
            actor,
            claim.status(),
            command.requestedDecision(),
            command.forceApproval()
        );

        if (!authorization.allowed()) {
            auditLogPort.append(command.claimId(), "CLAIM_DECISION_DENIED", actor.id().value(), command.correlationId());
            return ProcessClaimDecisionResult.builder(command.claimId())
                .previousStatus(claim.status())
                .newStatus(claim.status())
                .reason(authorization.asDecisionReason())
                .managerEscalationRequired(false)
                .customerNotificationRequired(false)
                .paymentPreparationRequired(false)
                .emit("CLAIM_DECISION_DENIED")
                .build();
        }

        CustomerSnapshot customer = customerMasterDataPort.loadCustomer(claim.customerId());
        DocumentVerificationSummary documents = documentVerificationPort.verifyRequiredDocuments(claim.id());
        FraudRiskAssessment fraudRisk = fraudRiskPort.assessRisk(claim.id(), customer);
        CoverageSnapshot coverageSnapshot = coveragePort.loadCoverage(claim.id(), claim.policyNumber());
        CoverageDecision coverage = coveragePolicy.decide(coverageSnapshot, claim.requestedAmount());

        TransitionDecision transition = transitionPolicy.decide(new ClaimTransitionContext(
            claim.status(),
            command.requestedDecision(),
            authorization,
            documents,
            fraudRisk,
            coverage
        ));

        claim.applyTransition(transition, command.comment(), actor.id());
        claimRepository.save(claim);

        auditLogPort.append(claim.id(), "CLAIM_STATUS_CHANGED", actor.id().value(), command.correlationId());

        ProcessClaimDecisionResult result = ProcessClaimDecisionResult.builder(claim.id())
            .previousStatus(claim.previousStatus())
            .newStatus(claim.status())
            .reason(transition.reason())
            .paymentPreparationRequired(claim.status().requiresPaymentPreparation())
            .customerNotificationRequired(claim.status().requiresCustomerNotification())
            .managerEscalationRequired(claim.status().requiresManagerEscalation())
            .emit("CLAIM_DECISION_PROCESSED")
            .build();

        if (result.paymentPreparationRequired()) {
            PaymentPreparationRequest request = PaymentPreparationFactory.from(claim, command);
            paymentReleasePort.preparePayment(request);
            outbox.enqueuePaymentPrepared(claim.id(), command.correlationId());
        }

        if (result.customerNotificationRequired()) {
            outbox.enqueueCustomerNotification(claim.id(), claim.customerId(), result.reason(), command.correlationId());
        }

        if (result.managerEscalationRequired()) {
            outbox.enqueueManagerEscalation(claim.id(), result.reason(), command.correlationId());
        }

        return result;
    }
}

Was ist hier bewusst noch nicht perfekt?

Der Handler ist schon viel besser als die Monster Method, aber noch relativ breit. In spÀteren Runden kann daraus ein ClaimDecisionWorkflowOrchestrator mit einzelnen Schritten entstehen.

Pattern / Konzept

Tests

Risiko

Wenn Payment noch synchron ist, bleibt ein technisches Risiko im Use Case. SpĂ€ter kann Payment vollstĂ€ndig ĂŒber Outbox/Worker entkoppelt werden.

Schritt 14: Workflow Orchestrator einfĂŒhren

FĂŒr sehr komplexe AblĂ€ufe wird der Handler schlanker gemacht. Der Ablauf wird in Schritte zerlegt.

Datei:

claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowOrchestrator.java
package com.seb4u.demo.claims.workflow;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;

import java.util.List;

public final class ClaimDecisionWorkflowOrchestrator {

    private final List<ClaimDecisionWorkflowStep> steps;

    public ClaimDecisionWorkflowOrchestrator(List<ClaimDecisionWorkflowStep> steps) {
        this.steps = List.copyOf(steps);
    }

    public ProcessClaimDecisionResult run(ProcessClaimDecisionCommand command, ClaimDecisionWorkflowContext context) {
        ClaimDecisionWorkflowContext current = context;

        for (ClaimDecisionWorkflowStep step : steps) {
            ClaimDecisionStepResult stepResult = step.execute(command, current);
            current = stepResult.context();

            if (stepResult.stopWorkflow()) {
                return stepResult.toProcessResult();
            }
        }

        return current.toProcessResult();
    }
}

Datei:

claim-workflow/src/main/java/com/seb4u/demo/claims/workflow/ClaimDecisionWorkflowStep.java
package com.seb4u.demo.claims.workflow;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;

public interface ClaimDecisionWorkflowStep {

    String name();

    ClaimDecisionStepResult execute(
        ProcessClaimDecisionCommand command,
        ClaimDecisionWorkflowContext context
    );
}

Beispiel-Schritt:

package com.seb4u.demo.claims.workflow.steps;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.domain.security.AuthorizationDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.shared.security.SecurityContextPort;
import com.seb4u.demo.claims.workflow.ClaimDecisionStepResult;
import com.seb4u.demo.claims.workflow.ClaimDecisionWorkflowContext;
import com.seb4u.demo.claims.workflow.ClaimDecisionWorkflowStep;

public final class AuthorizationWorkflowStep implements ClaimDecisionWorkflowStep {

    private final SecurityContextPort securityContextPort;
    private final AuthorizationPolicy authorizationPolicy;

    public AuthorizationWorkflowStep(
            SecurityContextPort securityContextPort,
            AuthorizationPolicy authorizationPolicy) {
        this.securityContextPort = securityContextPort;
        this.authorizationPolicy = authorizationPolicy;
    }

    @Override
    public String name() {
        return "authorization";
    }

    @Override
    public ClaimDecisionStepResult execute(
            ProcessClaimDecisionCommand command,
            ClaimDecisionWorkflowContext context) {
        var actor = securityContextPort.currentActor();
        AuthorizationDecision decision = authorizationPolicy.mayProcessDecision(
            actor,
            context.claim().status(),
            command.requestedDecision(),
            command.forceApproval()
        );

        ClaimDecisionWorkflowContext updated = context.withActor(actor).withAuthorization(decision);

        if (!decision.allowed()) {
            return ClaimDecisionStepResult.stop(updated, decision.asDecisionReason());
        }

        return ClaimDecisionStepResult.continueWith(updated);
    }
}

Warum wird das gemacht?

Ein Orchestrator hilft, wenn der Ablauf fachlich lang ist, aber jeder Schritt einzeln testbar sein soll.

Pattern / Konzept

Tests

Risiko

Ein Orchestrator kann selbst wieder zu einer versteckten Workflow Engine werden. Deshalb mĂŒssen Schritte klein, benannt und dokumentiert bleiben.

Teil G: Audit, Outbox, Notification und Search

Schritt 15: Audit Trail abstrahieren

Datei:

claim-audit/src/main/java/com/seb4u/demo/claims/audit/AuditLogPort.java
package com.seb4u.demo.claims.audit;

import com.seb4u.demo.claims.domain.model.ClaimId;

public interface AuditLogPort {

    void append(ClaimId claimId, String eventType, String actorId, String correlationId);
}

Datei:

claim-audit/src/main/java/com/seb4u/demo/claims/audit/StructuredAuditLogAdapter.java
package com.seb4u.demo.claims.audit;

import com.seb4u.demo.claims.domain.model.ClaimId;

import java.time.Clock;
import java.time.Instant;

public final class StructuredAuditLogAdapter implements AuditLogPort {

    private final AuditEventRepository repository;
    private final Clock clock;

    public StructuredAuditLogAdapter(AuditEventRepository repository, Clock clock) {
        this.repository = repository;
        this.clock = clock;
    }

    @Override
    public void append(ClaimId claimId, String eventType, String actorId, String correlationId) {
        AuditEvent event = new AuditEvent(
            claimId.value(),
            eventType,
            actorId,
            correlationId,
            Instant.now(clock)
        );
        repository.save(event);
    }
}

Warum wurde es geÀndert?

Audit ist fachlich wichtig und darf nicht nur ein technisches Log sein. Es muss nachvollziehbar, testbar und spÀter reportbar sein.

Schritt 16: Outbox Pattern einfĂŒhren

Was war vorher schlecht?

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

Was wird geÀndert?

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

Datei:

claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxEvent.java
package com.seb4u.demo.claims.notification.outbox;

import java.time.Instant;
import java.util.UUID;

public final class OutboxEvent {

    private final UUID id;
    private final String aggregateId;
    private final String eventType;
    private final String payloadJson;
    private final String correlationId;
    private final Instant createdAt;
    private final int publishAttempts;

    public OutboxEvent(
            UUID id,
            String aggregateId,
            String eventType,
            String payloadJson,
            String correlationId,
            Instant createdAt,
            int publishAttempts) {
        this.id = id;
        this.aggregateId = aggregateId;
        this.eventType = eventType;
        this.payloadJson = payloadJson;
        this.correlationId = correlationId;
        this.createdAt = createdAt;
        this.publishAttempts = publishAttempts;
    }

    public UUID id() {
        return id;
    }

    public String aggregateId() {
        return aggregateId;
    }

    public String eventType() {
        return eventType;
    }

    public String payloadJson() {
        return payloadJson;
    }

    public String correlationId() {
        return correlationId;
    }

    public Instant createdAt() {
        return createdAt;
    }

    public int publishAttempts() {
        return publishAttempts;
    }
}

Datei:

claim-notification/src/main/java/com/seb4u/demo/claims/notification/outbox/OutboxPublisherWorker.java
package com.seb4u.demo.claims.notification.outbox;

import java.util.List;

public final class OutboxPublisherWorker {

    private final OutboxRepository repository;
    private final MessagePublisher publisher;

    public OutboxPublisherWorker(OutboxRepository repository, MessagePublisher publisher) {
        this.repository = repository;
        this.publisher = publisher;
    }

    public void publishNextBatch() {
        List<OutboxEvent> events = repository.lockNextUnpublishedBatch(100);

        for (OutboxEvent event : events) {
            try {
                publisher.publish(event.eventType(), event.payloadJson(), event.correlationId());
                repository.markPublished(event.id());
            } catch (Exception ex) {
                repository.markFailedAttempt(event.id(), ex.getMessage());
            }
        }
    }
}

Pattern / Konzept

Tests

Schritt 17: Notification als Port

Datei:

claim-notification/src/main/java/com/seb4u/demo/claims/notification/NotificationPort.java
package com.seb4u.demo.claims.notification;

import com.seb4u.demo.claims.domain.model.ClaimId;

public interface NotificationPort {

    void sendClaimStatusNotification(ClaimId claimId, NotificationRecipient recipient, NotificationTemplate template);
}

Der wichtige Punkt:

Im Use Case wird nicht direkt E-Mail oder SMS gesendet. Stattdessen wird ein Outbox Event erzeugt, das spÀter ein Notification Adapter verarbeitet.

Schritt 18: Search und Reporting als Read Models

Was war vorher schlecht?

Suche lief ĂŒber Oracle LIKE Queries direkt aus dem Backoffice.

Was wird geÀndert?

Ein ClaimSearchPort trennt die SuchdomÀne.

Datei:

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

public interface ClaimSearchPort {

    ClaimSearchResult search(ClaimSearchQuery query);
}

Datei:

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

import java.time.LocalDate;
import java.util.Set;

public record ClaimSearchQuery(
    String freeText,
    Set<String> statuses,
    String customerId,
    String partnerId,
    LocalDate createdFrom,
    LocalDate createdTo,
    int page,
    int size
) {
}

Migrationnutzen

Heute kann der Adapter weiterhin Oracle nutzen. SpÀter kann er OpenSearch oder Elasticsearch verwenden, ohne dass Backoffice oder Use Case geÀndert werden.

Teil H: Legacy UI entkoppeln

Schritt 19: Partner Portal vom EJB entkoppeln

Vorher

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

Nachher schrittweise

LegacyPartnerPortalServlet
  -> PartnerPortalClaimApplicationService
  -> ProcessClaimDecisionHandler

SpÀter:

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

Datei:

legacy-claims-partner-portal/src/main/java/com/seb4u/demo/claims/portal/PartnerPortalClaimApplicationService.java
package com.seb4u.demo.claims.portal;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.shared.security.ActorId;

public final class PartnerPortalClaimApplicationService {

    private final ProcessClaimDecisionHandler handler;
    private final PartnerPortalCommandMapper mapper;

    public PartnerPortalClaimApplicationService(
            ProcessClaimDecisionHandler handler,
            PartnerPortalCommandMapper mapper) {
        this.handler = handler;
        this.mapper = mapper;
    }

    public PartnerPortalDecisionResponse submitDecision(PartnerPortalDecisionRequest request) {
        ProcessClaimDecisionCommand command = ProcessClaimDecisionCommand.builder()
            .claimId(ClaimId.of(request.claimId()))
            .requestedDecision(RequestedDecision.fromLegacyCode(request.decision()))
            .actorId(ActorId.of(request.partnerUserId()))
            .actorRoles(mapper.mapRoles(request.legacyRoles()))
            .comment(mapper.mapComment(request.comment()))
            .forceApproval(false)
            .correlationId(request.correlationId())
            .build();

        ProcessClaimDecisionResult result = handler.handle(command);
        return mapper.toPortalResponse(result);
    }
}

Pattern / Konzept

Risiko

Partner Portal hat oft Session- und Rollenlogik. Diese muss langsam entkoppelt werden, sonst entstehen SicherheitslĂŒcken.

Schritt 20: Internal Backoffice entkoppeln

Backoffice bekommt eine eigene Use-Case-Schicht, statt direkt EJB und Stored Procedures aufzurufen.

Datei:

legacy-internal-claims-backoffice/src/main/java/com/seb4u/demo/claims/backoffice/ClaimBackofficeUseCase.java
package com.seb4u.demo.claims.backoffice;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.search.ClaimSearchPort;
import com.seb4u.demo.claims.search.ClaimSearchQuery;
import com.seb4u.demo.claims.search.ClaimSearchResult;

public final class ClaimBackofficeUseCase {

    private final ClaimSearchPort searchPort;
    private final ProcessClaimDecisionHandler decisionHandler;
    private final BackofficeCommandMapper commandMapper;

    public ClaimBackofficeUseCase(
            ClaimSearchPort searchPort,
            ProcessClaimDecisionHandler decisionHandler,
            BackofficeCommandMapper commandMapper) {
        this.searchPort = searchPort;
        this.decisionHandler = decisionHandler;
        this.commandMapper = commandMapper;
    }

    public ClaimSearchResult searchClaims(BackofficeSearchRequest request) {
        ClaimSearchQuery query = commandMapper.toSearchQuery(request);
        return searchPort.search(query);
    }

    public BackofficeDecisionResponse processDecision(BackofficeDecisionRequest request) {
        ProcessClaimDecisionCommand command = commandMapper.toDecisionCommand(request);
        ProcessClaimDecisionResult result = decisionHandler.handle(command);
        return commandMapper.toDecisionResponse(result);
    }
}

Teil I: Batch, SLA und Eskalation

Schritt 21: SLA/Eskalation aus TimerBean lösen

Was war vorher schlecht?

Die WebSphere TimerBean hat direkt Oracle Queries, Stored Procedures und Notifications ausgelöst.

Was wird geÀndert?

SLA-Regeln werden fachlich modelliert. Scheduler wird Infrastruktur.

Datei:

claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationPolicy.java
package com.seb4u.demo.claims.batch.escalation;

import com.seb4u.demo.claims.domain.model.ClaimStatus;

import java.time.Duration;
import java.time.Instant;

public final class EscalationPolicy {

    public EscalationDecision decide(EscalationCandidate candidate, Instant now) {
        if (candidate.status() == ClaimStatus.CLOSED || candidate.status() == ClaimStatus.REJECTED) {
            return EscalationDecision.noEscalation("claim already finished");
        }

        Duration openDuration = Duration.between(candidate.lastStatusChangedAt(), now);

        if (candidate.status() == ClaimStatus.WAITING_FOR_DOCUMENTS
                && openDuration.toHours() > 72) {
            return EscalationDecision.escalateToPartnerSupport("documents missing for more than 72 hours");
        }

        if (candidate.status() == ClaimStatus.PENDING_REVIEW
                && openDuration.toHours() > 48) {
            return EscalationDecision.escalateToTeamLead("pending review exceeds 48 hours");
        }

        if (candidate.status() == ClaimStatus.MANUAL_FRAUD_REVIEW
                && openDuration.toHours() > 24) {
            return EscalationDecision.escalateToFraudSpecialist("fraud review exceeds 24 hours");
        }

        return EscalationDecision.noEscalation("SLA still within limits");
    }
}

Datei:

claim-batch/src/main/java/com/seb4u/demo/claims/batch/escalation/EscalationBatchJob.java
package com.seb4u.demo.claims.batch.escalation;

import com.seb4u.demo.claims.notification.outbox.OutboxRepository;

import java.time.Clock;
import java.time.Instant;
import java.util.List;

public final class EscalationBatchJob {

    private final EscalationCandidateRepository candidateRepository;
    private final EscalationPolicy escalationPolicy;
    private final OutboxRepository outboxRepository;
    private final LeaseLock leaseLock;
    private final Clock clock;

    public EscalationBatchJob(
            EscalationCandidateRepository candidateRepository,
            EscalationPolicy escalationPolicy,
            OutboxRepository outboxRepository,
            LeaseLock leaseLock,
            Clock clock) {
        this.candidateRepository = candidateRepository;
        this.escalationPolicy = escalationPolicy;
        this.outboxRepository = outboxRepository;
        this.leaseLock = leaseLock;
        this.clock = clock;
    }

    public void run() {
        if (!leaseLock.tryAcquire("claim-escalation-job")) {
            return;
        }

        try {
            Instant now = Instant.now(clock);
            List<EscalationCandidate> candidates = candidateRepository.findOpenCandidates(500);

            for (EscalationCandidate candidate : candidates) {
                EscalationDecision decision = escalationPolicy.decide(candidate, now);
                if (decision.escalationRequired()) {
                    candidateRepository.markEscalated(candidate.claimId(), decision.targetQueue(), now);
                    outboxRepository.enqueueEscalation(candidate.claimId().value(), decision.reason());
                }
            }
        } finally {
            leaseLock.release("claim-escalation-job");
        }
    }
}

Pattern / Konzept

Risiko

Mehrere Pod-Instanzen können denselben Batch ausfĂŒhren. Deshalb ist ein Lease Lock wichtig.

Teil J: Architekturtests

Schritt 22: ArchUnit-Regeln

Ziel: verhindern, dass nach dem Refactoring wieder technische AbhĂ€ngigkeiten in die Domain zurĂŒckwandern.

Datei:

migration-tests/src/test/java/com/seb4u/demo/claims/architecture/ArchitectureBoundaryTest.java
package com.seb4u.demo.claims.architecture;

import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

class ArchitectureBoundaryTest {

    @Test
    void domainMustNotDependOnInfrastructureOrJavaEe() {
        var classes = new ClassFileImporter()
            .importPackages("com.seb4u.demo.claims");

        ArchRule rule = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat().resideInAnyPackage(
                "..infrastructure..",
                "javax.ejb..",
                "javax.jms..",
                "javax.servlet..",
                "javax.xml.ws..",
                "oracle.jdbc.."
            );

        rule.check(classes);
    }

    @Test
    void applicationMustUsePortsInsteadOfSoapClientsDirectly() {
        var classes = new ClassFileImporter()
            .importPackages("com.seb4u.demo.claims");

        ArchRule rule = noClasses()
            .that().resideInAPackage("..application..")
            .should().dependOnClassesThat().haveSimpleNameEndingWith("SoapClient");

        rule.check(classes);
    }
}

Warum wichtig?

Refactoring ist kein einmaliges Ereignis. Architekturregeln verhindern RĂŒckfĂ€lle.

Teil K: Golden Master Vergleich Alt gegen Neu

Legacy vs. Modular Handler Vergleichstest

Datei:

migration-tests/src/test/java/com/seb4u/demo/claims/migration/LegacyVsModularDecisionComparisonTest.java
package com.seb4u.demo.claims.migration;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.legacy.ejb.LegacyClaimFacadeBean;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

class LegacyVsModularDecisionComparisonTest {

    static Stream<GoldenMasterCase> goldenMasterCases() {
        return GoldenMasterCaseLoader.load("golden-master/claim-decision-cases.yml").stream();
    }

    @ParameterizedTest(name = "{0}")
    @MethodSource("goldenMasterCases")
    void modularHandlerKeepsBusinessRelevantLegacyBehaviour(GoldenMasterCase testCase) throws Exception {
        LegacyClaimFacadeBean legacy = LegacyFixtureFactory.legacyFacadeFor(testCase);
        ProcessClaimDecisionHandler modular = ModularFixtureFactory.handlerFor(testCase);

        GoldenMasterSnapshot legacySnapshot = LegacyRunner.run(legacy, testCase);

        ProcessClaimDecisionCommand command = testCase.toCommand();
        GoldenMasterSnapshot modularSnapshot = ModularRunner.run(modular, command);

        assertThat(modularSnapshot.finalStatus()).isEqualTo(legacySnapshot.finalStatus());
        assertThat(modularSnapshot.auditEvents()).containsAll(legacySnapshot.auditEvents());
        assertThat(modularSnapshot.outboundEvents()).containsAll(legacySnapshot.outboundEvents());
    }
}

Wichtig

Der neue Code muss nicht jede technische Nebenwirkung identisch machen. Er muss die fachlich relevanten Ergebnisse kompatibel halten.

Beispiel:

Teil L: Refactoring-Matrix dieser Runde

Refactoring-Schritte als Matrix

IDÄnderungVorherNachherPattern / KonzeptTest / SicherheitsnetzRisiko
R3-01Characterization Testskein SicherheitsnetzGolden Master CasesCharacterization TestLegacy Snapshot Testschlechtes Verhalten wird konserviert
R3-02Parameter Objectviele PrimitiveLegacyDecisionInputParameter Objectbestehender Legacy TestVerschieben kann Seiteneffekte Àndern
R3-03Command ObjectSOAP/EJB ParameterProcessClaimDecisionCommandCommand PatternMapping Testzu frĂŒhe Typisierung
R3-04Result ObjectString/ExceptionProcessClaimDecisionResultResult ObjectResponse Mapping TestLegacy Fehlercodes
R3-05Statuslogikif/elseClaimTransitionPolicyState/PolicyTransition TestsReihenfolge der Regeln
R3-06Document PortFile Share direktDocumentVerificationPortPort/AdapterAdapter TestOpenShift Storage
R3-07Fraud PortSOAP direktFraudRiskPortACLTimeout Teststechnische Fehlerklassifikation
R3-08Coverage PolicySOAP/SP gemischtCoveragePolicySpecificationCoverage Testsalte Tariflogik unvollstÀndig
R3-09Payment PortSOAP/JTA direktPaymentReleasePortIdempotencyPayment TestsDoppelzahlung
R3-10Security PolicyJAAS direktAuthorizationPolicyPolicyAuth TestsRole Mapping
R3-11WorkflowEJB AblaufOrchestratorWorkflowStep Testszu komplexe Mini-Engine
R3-12AuditSP/LogsAuditLogPortAudit EventAudit TestsCompliance-Anforderungen
R3-13OutboxJMS direktOutbox WorkerOutboxPublish TestsEventual Consistency
R3-14BatchTimerBeanBatch WorkerLease LockBatch TestsParallelitÀt in Pods
R3-15UI EntkopplungJSP direkt an EJBUse Case / RESTStranglerUI Adapter TestsSession/Security

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

Mapping-Kandidaten

Legacy-Datei / ModulNeuer Ziel-OrtGrundPattern / KonzeptSicherheitsnetz
LegacyClaimFacadeBean.javaProcessClaimDecisionHandler.javaMonster Method zerlegenCommand HandlerGolden Master + Handler Test
LegacyClaimStatusIfElse.javaClaimTransitionPolicy.javaStatuslogik isolierenState/PolicyTransition Test
LegacyDocumentShareClient.javaDocumentVerificationPort.java + FileShareDocumentVerificationAdapter.javaFile Share entkoppelnPort/AdapterAdapter Test
FraudRiskSoapClient.javaFraudRiskPort.java + FraudRiskSoapAdapter.javaSOAP entkoppelnACLTimeout/Fallback Test
PolicyCoverageSoapClient.javaCoveragePort.java + CoveragePolicy.javatechnische und fachliche Coverage trennenSpecificationCoverage Test
PaymentReleaseSoapClient.javaPaymentReleasePort.javaZahlung absichernIdempotencyIdempotency Test
LegacyNotificationSenderBean.javaNotificationPort.java + OutboxNebenwirkung trennenOutboxOutbox Test
SlaEscalationTimerBean.javaEscalationBatchJob.java + EscalationPolicy.javaScheduler entkoppelnBatch WorkerBatch Test
AuditReportStoredProcedureDao.javaAuditLogPort.java + AuditReportingAdapter.javaAudit sichtbar machenAudit EventAudit Test
LegacyClaimSearchDao.javaClaimSearchPort.javaSuche trennenRead ModelSearch Adapter Test
CustomerMasterDataSoapClient.javaCustomerMasterDataPort.javaStammdaten entkoppelnACLContract Test
JaasRoleChecker.javaSecurityContextPort.java + AuthorizationPolicy.javaAuthN/AuthZ trennenPolicyAuthorization 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:

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

EbeneTesttypZweckBeispiel
DomainUnit TestsFachregeln prĂŒfenClaimTransitionPolicyTest
ApplicationHandler TestsUse Case prĂŒfenProcessClaimDecisionHandlerTest
AdapterAdapter Teststechnische Übersetzung prĂŒfenFraudRiskSoapAdapterTest
ContractSOAP/REST Contract TestsSchnittstellen stabil haltenClaimSoapCompatibilityTest
MigrationGolden Master TestsLegacy vs. Neu vergleichenLegacyVsModularDecisionComparisonTest
ArchitectureArchUnitGrenzen sichernArchitectureBoundaryTest
BatchBatch TestsSLA/Eskalation prĂŒfenEscalationBatchJobTest
OpenShift spĂ€terSmoke TestsRuntime prĂŒfenClaimRuntimeSmokeTest

Handler Test mit Fakes

Datei:

claim-application/src/test/java/com/seb4u/demo/claims/application/decision/ProcessClaimDecisionHandlerTest.java
package com.seb4u.demo.claims.application.decision;

import com.seb4u.demo.claims.application.testing.FakeAuditLogPort;
import com.seb4u.demo.claims.application.testing.FakeClaimRepository;
import com.seb4u.demo.claims.application.testing.FakeCoveragePort;
import com.seb4u.demo.claims.application.testing.FakeCustomerMasterDataPort;
import com.seb4u.demo.claims.application.testing.FakeDocumentVerificationPort;
import com.seb4u.demo.claims.application.testing.FakeFraudRiskPort;
import com.seb4u.demo.claims.application.testing.FakeOutbox;
import com.seb4u.demo.claims.application.testing.FakePaymentReleasePort;
import com.seb4u.demo.claims.application.testing.FakeSecurityContextPort;
import com.seb4u.demo.claims.domain.coverage.CoveragePolicy;
import com.seb4u.demo.claims.domain.model.Claim;
import com.seb4u.demo.claims.domain.model.ClaimId;
import com.seb4u.demo.claims.domain.model.RequestedDecision;
import com.seb4u.demo.claims.domain.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.workflow.ClaimTransitionPolicy;
import com.seb4u.demo.claims.shared.security.ActorId;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ProcessClaimDecisionHandlerTest {

    @Test
    void approvesCoveredLowRiskClaimAndCreatesPaymentOutboxEvent() {
        ClaimId claimId = ClaimId.of("CLM-12345");
        FakeClaimRepository claims = new FakeClaimRepository()
            .withClaim(Claim.pendingReview(claimId, "CUST-4711", "POL-2024-0001", "1200.00"));

        FakeOutbox outbox = new FakeOutbox();
        FakePaymentReleasePort payment = new FakePaymentReleasePort();
        FakeAuditLogPort audit = new FakeAuditLogPort();

        ProcessClaimDecisionHandler handler = new ProcessClaimDecisionHandler(
            claims,
            FakeCustomerMasterDataPort.standardCustomer(),
            FakeSecurityContextPort.manager("manager-1"),
            new AuthorizationPolicy(),
            FakeDocumentVerificationPort.allVerified(),
            FakeFraudRiskPort.lowRisk(12),
            FakeCoveragePort.covered(),
            new CoveragePolicy(),
            new ClaimTransitionPolicy(),
            payment,
            audit,
            outbox
        );

        ProcessClaimDecisionCommand command = ProcessClaimDecisionCommand.builder()
            .claimId(claimId)
            .requestedDecision(RequestedDecision.APPROVE)
            .actorId(ActorId.of("manager-1"))
            .actorRoles(FakeSecurityContextPort.managerRoles())
            .comment("approved after review")
            .forceApproval(false)
            .correlationId("corr-123")
            .build();

        ProcessClaimDecisionResult result = handler.handle(command);

        assertThat(result.newStatus().name()).isEqualTo("PAYMENT_PENDING");
        assertThat(payment.preparedPayments()).hasSize(1);
        assertThat(outbox.events()).extracting("eventType")
            .contains("PAYMENT_PREPARED");
        assertThat(audit.events()).extracting("eventType")
            .contains("CLAIM_DECISION_STARTED", "CLAIM_STATUS_CHANGED");
    }
}

Teil P: Risiken nach Runde 3

Noch offene technische Risiken

RisikoBeschreibungGegenmaßnahme
Hidden Stored Procedure Logicalte Stored Procedures enthalten FachlogikDB-Inventory + Characterization Tests
SOAP Timeout Semantikunklar, ob Operation ausgefĂŒhrt wurdeIdempotency + Contract Tests
Payment DoppelverarbeitungRetry kann Zahlung doppelt vorbereitenIdempotency Key + Payment Status Check
JAAS Rollenmappingmoderne Rollen weichen abRole Mapping Dokument + Auth Tests
File Share auf OpenShiftlokale Pfade funktionieren nicht stabilDocument Port + Object Storage Adapter spÀter
Audit Complianceneues Audit muss regulatorisch reichenAudit Event Catalog + Compliance Review
Eventual ConsistencyOutbox entkoppelt NebenwirkungenUI-Status klar anzeigen, Retry/Dead Letter
Batch ParallelitĂ€tmehrere Pods können Job parallel ausfĂŒhrenLease Lock
Legacy UI SessionJSP-Session kann Auth-Kontext versteckenStrangler UI Adapter + SecurityContextPort

Teil Q: Ergebnis dieser Runde

Ergebnis

Runde 3 hat den Refactoring-Lerninhalt aufgebaut:

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:

Wichtige Codebeispiele dieser Runde

Runde-3-MerksÀtze

Ende Runde 03.

5. Moderne Zielarchitektur und Maven-Struktur

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Moderne Zielarchitektur und Maven-Struktur in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Große Runde 4: Modernes Zielprojekt mit modularer Maven-Struktur

Diese Runde beschreibt das moderne Zielprojekt nach dem Refactoring. Der Fokus liegt auf einer modularen Maven-Struktur, klaren fachlichen Grenzen, testbarer Fachlogik, kompatiblen Legacy-Boundaries und vorbereiteter Migration nach OpenShift.

Die Runde baut auf den vorherigen Runden auf:

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

Empfohlene Root-Struktur:

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

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

Architekturprinzipien der Zielstruktur

Domain bleibt frei von Infrastruktur

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

Erlaubt sind:

Application orchestriert Use Cases

Das Modul claim-application kennt:

Es kennt nicht direkt:

Infrastructure implementiert technische Details

Das Modul claim-infrastructure implementiert Ports:

API-Module bleiben dĂŒnn

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

Sie tun nur:

Legacy-Module bleiben als Lern- und Migrationsanker erhalten

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

Moderne Maven Parent POM

Datei:

pom.xml

Beispiel:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.seb4u.demo.claims</groupId>
  <artifactId>legacy-claims-customer-support-enterprise</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>Legacy Claims & Customer Support Enterprise System</name>
  <description>Enterprise learning project for refactoring, modernization and OpenShift migration.</description>

  <properties>
    <java.version>17</java.version>
    <maven.compiler.release>${java.version}</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <junit.version>5.10.2</junit.version>
    <assertj.version>3.25.3</assertj.version>
    <mockito.version>5.11.0</mockito.version>
    <archunit.version>1.2.1</archunit.version>

    <jakarta.version>10.0.0</jakarta.version>
    <spring.boot.version>3.3.0</spring.boot.version>
    <maven.enforcer.version>3.4.1</maven.enforcer.version>
    <maven.compiler.version>3.13.0</maven.compiler.version>
    <maven.surefire.version>3.2.5</maven.surefire.version>
    <maven.failsafe.version>3.2.5</maven.failsafe.version>
    <jacoco.version>0.8.12</jacoco.version>
  </properties>

  <modules>
    <module>modules/shared-kernel</module>
    <module>modules/claim-domain</module>
    <module>modules/claim-application</module>
    <module>modules/claim-workflow</module>
    <module>modules/claim-document</module>
    <module>modules/claim-search</module>
    <module>modules/claim-notification</module>
    <module>modules/claim-audit</module>
    <module>modules/claim-payment</module>
    <module>modules/claim-infrastructure</module>
    <module>modules/claim-soap-api</module>
    <module>modules/claim-rest-api</module>
    <module>modules/claim-openapi</module>
    <module>modules/claim-batch</module>
    <module>modules/migration-tests</module>
  </modules>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>${junit.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring.boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>${maven.compiler.version}</version>
          <configuration>
            <release>${maven.compiler.release}</release>
            <compilerArgs>
              <arg>-Xlint:unchecked</arg>
              <arg>-Xlint:deprecation</arg>
            </compilerArgs>
          </configuration>
        </plugin>

        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>${maven.surefire.version}</version>
          <configuration>
            <useModulePath>false</useModulePath>
          </configuration>
        </plugin>

        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-failsafe-plugin</artifactId>
          <version>${maven.failsafe.version}</version>
        </plugin>

        <plugin>
          <groupId>org.jacoco</groupId>
          <artifactId>jacoco-maven-plugin</artifactId>
          <version>${jacoco.version}</version>
        </plugin>
      </plugins>
    </pluginManagement>

    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>${maven.enforcer.version}</version>
        <executions>
          <execution>
            <id>enforce-build-rules</id>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <requireJavaVersion>
                  <version>[17,)</version>
                </requireJavaVersion>
                <requireMavenVersion>
                  <version>[3.9,)</version>
                </requireMavenVersion>
                <banDuplicatePomDependencyVersions/>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Warum Java 17?

Modul: shared-kernel

Zweck

shared-kernel enthÀlt nur wirklich gemeinsam verwendbare Basistypen.

Nicht hinein gehören:

Hinein gehören:

POM

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.seb4u.demo.claims</groupId>
    <artifactId>legacy-claims-customer-support-enterprise</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>shared-kernel</artifactId>
  <packaging>jar</packaging>
</project>

Code: ClaimId

package com.seb4u.demo.claims.sharedkernel;

import java.util.Objects;
import java.util.UUID;

public final class ClaimId {

    private final String value;

    private ClaimId(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("ClaimId must not be blank");
        }
        this.value = value;
    }

    public static ClaimId of(String value) {
        return new ClaimId(value);
    }

    public static ClaimId newId() {
        return new ClaimId("CLM-" + UUID.randomUUID());
    }

    public String value() {
        return value;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof ClaimId claimId)) {
            return false;
        }
        return Objects.equals(value, claimId.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return value;
    }
}

Code: BusinessClock

package com.seb4u.demo.claims.sharedkernel;

import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public final class BusinessClock {

    private final Clock clock;

    public BusinessClock(Clock clock) {
        this.clock = clock;
    }

    public static BusinessClock systemUtc() {
        return new BusinessClock(Clock.systemUTC());
    }

    public Instant now() {
        return Instant.now(clock);
    }

    public LocalDate today(ZoneId zoneId) {
        return LocalDate.now(clock.withZone(zoneId));
    }
}

Modul: claim-domain

Zweck

claim-domain enthÀlt die eigentliche Claim-Fachlichkeit:

Es kennt keine technischen Adapter.

POM

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.seb4u.demo.claims</groupId>
    <artifactId>legacy-claims-customer-support-enterprise</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>claim-domain</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>shared-kernel</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

Code: ClaimStatus

package com.seb4u.demo.claims.domain;

public enum ClaimStatus {
    CREATED,
    DOCUMENTS_PENDING,
    DOCUMENTS_RECEIVED,
    DOCUMENTS_VERIFIED,
    COVERAGE_CHECKED,
    FRAUD_REVIEW_REQUIRED,
    MANUAL_REVIEW_REQUIRED,
    APPROVAL_REQUIRED,
    APPROVED,
    REJECTED,
    PAYMENT_RELEASE_REQUESTED,
    PAYMENT_RELEASED,
    ESCALATED,
    CLOSED
}

Code: ClaimDecisionType

package com.seb4u.demo.claims.domain;

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

Code: ClaimTransitionPolicy

package com.seb4u.demo.claims.domain;

import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

public final class ClaimTransitionPolicy {

    private final Map<ClaimStatus, Set<ClaimDecisionType>> allowedDecisions;

    public ClaimTransitionPolicy() {
        this.allowedDecisions = new EnumMap<>(ClaimStatus.class);
        allowedDecisions.put(ClaimStatus.CREATED, EnumSet.of(
                ClaimDecisionType.REQUEST_MORE_DOCUMENTS,
                ClaimDecisionType.SEND_TO_MANUAL_REVIEW
        ));
        allowedDecisions.put(ClaimStatus.DOCUMENTS_RECEIVED, EnumSet.of(
                ClaimDecisionType.APPROVE,
                ClaimDecisionType.REJECT,
                ClaimDecisionType.ESCALATE,
                ClaimDecisionType.SEND_TO_MANUAL_REVIEW
        ));
        allowedDecisions.put(ClaimStatus.DOCUMENTS_VERIFIED, EnumSet.of(
                ClaimDecisionType.APPROVE,
                ClaimDecisionType.REJECT,
                ClaimDecisionType.ESCALATE
        ));
        allowedDecisions.put(ClaimStatus.APPROVED, EnumSet.of(
                ClaimDecisionType.RELEASE_PAYMENT
        ));
    }

    public void assertDecisionAllowed(ClaimStatus currentStatus, ClaimDecisionType decisionType) {
        Set<ClaimDecisionType> decisions = allowedDecisions.getOrDefault(currentStatus, Set.of());
        if (!decisions.contains(decisionType)) {
            throw new InvalidClaimTransitionException(
                    "Decision " + decisionType + " is not allowed for current status " + currentStatus
            );
        }
    }

    public ClaimStatus nextStatus(ClaimStatus currentStatus, ClaimDecisionType decisionType) {
        assertDecisionAllowed(currentStatus, decisionType);

        return switch (decisionType) {
            case APPROVE -> ClaimStatus.APPROVED;
            case REJECT -> ClaimStatus.REJECTED;
            case REQUEST_MORE_DOCUMENTS -> ClaimStatus.DOCUMENTS_PENDING;
            case ESCALATE -> ClaimStatus.ESCALATED;
            case SEND_TO_MANUAL_REVIEW -> ClaimStatus.MANUAL_REVIEW_REQUIRED;
            case RELEASE_PAYMENT -> ClaimStatus.PAYMENT_RELEASE_REQUESTED;
        };
    }
}

Code: Claim Aggregate

package com.seb4u.demo.claims.domain;

import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public final class Claim {

    private final ClaimId id;
    private final String customerNumber;
    private final BigDecimal requestedAmount;
    private ClaimStatus status;
    private final List<ClaimDomainEvent> domainEvents = new ArrayList<>();

    public Claim(ClaimId id,
                 String customerNumber,
                 BigDecimal requestedAmount,
                 ClaimStatus status) {
        this.id = Objects.requireNonNull(id);
        this.customerNumber = requireText(customerNumber, "customerNumber");
        this.requestedAmount = Objects.requireNonNull(requestedAmount);
        this.status = Objects.requireNonNull(status);
    }

    public ClaimId id() {
        return id;
    }

    public String customerNumber() {
        return customerNumber;
    }

    public BigDecimal requestedAmount() {
        return requestedAmount;
    }

    public ClaimStatus status() {
        return status;
    }

    public void applyDecision(ClaimDecision decision,
                              ClaimTransitionPolicy transitionPolicy,
                              Instant decidedAt) {
        Objects.requireNonNull(decision);
        Objects.requireNonNull(transitionPolicy);
        Objects.requireNonNull(decidedAt);

        ClaimStatus oldStatus = this.status;
        ClaimStatus newStatus = transitionPolicy.nextStatus(oldStatus, decision.type());
        this.status = newStatus;

        domainEvents.add(new ClaimDecisionApplied(
                id,
                oldStatus,
                newStatus,
                decision.type(),
                decision.decidedBy(),
                decidedAt
        ));
    }

    public List<ClaimDomainEvent> pullDomainEvents() {
        List<ClaimDomainEvent> copy = List.copyOf(domainEvents);
        domainEvents.clear();
        return copy;
    }

    public List<ClaimDomainEvent> peekDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }

    private static String requireText(String value, String fieldName) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(fieldName + " must not be blank");
        }
        return value;
    }
}

Code: ClaimDecision

package com.seb4u.demo.claims.domain;

public record ClaimDecision(
        ClaimDecisionType type,
        String reasonCode,
        String comment,
        String decidedBy
) {
    public ClaimDecision {
        if (type == null) {
            throw new IllegalArgumentException("type must not be null");
        }
        if (decidedBy == null || decidedBy.isBlank()) {
            throw new IllegalArgumentException("decidedBy must not be blank");
        }
    }
}

Modul: claim-application

Zweck

claim-application enthÀlt die Use Cases und Ports.

Typische Inhalte:

POM

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.seb4u.demo.claims</groupId>
    <artifactId>legacy-claims-customer-support-enterprise</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>claim-application</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>shared-kernel</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>claim-domain</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

Code: ProcessClaimDecisionCommand

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

import com.seb4u.demo.claims.domain.ClaimDecisionType;
import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.math.BigDecimal;
import java.util.List;

public record ProcessClaimDecisionCommand(
        ClaimId claimId,
        String customerNumber,
        String policyNumber,
        ClaimDecisionType decisionType,
        BigDecimal requestedAmount,
        String reasonCode,
        String comment,
        String actorUserId,
        List<String> documentIds,
        String idempotencyKey
) {
    public ProcessClaimDecisionCommand {
        if (claimId == null) {
            throw new IllegalArgumentException("claimId must not be null");
        }
        if (decisionType == null) {
            throw new IllegalArgumentException("decisionType must not be null");
        }
        if (actorUserId == null || actorUserId.isBlank()) {
            throw new IllegalArgumentException("actorUserId must not be blank");
        }
        documentIds = documentIds == null ? List.of() : List.copyOf(documentIds);
    }
}

Code: Port ClaimRepository

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

import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.util.Optional;

public interface ClaimRepository {
    Optional<Claim> findById(ClaimId claimId);
    void save(Claim claim);
}

Code: Ports fĂŒr Zusatzsysteme

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

import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.util.List;

public interface DocumentVerificationPort {
    DocumentVerificationResult verifyDocuments(ClaimId claimId, List<String> documentIds);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.sharedkernel.ClaimId;

public interface FraudRiskPort {
    FraudRiskResult assessRisk(ClaimId claimId, String customerNumber, String policyNumber);
}
package com.seb4u.demo.claims.application.port;

import java.math.BigDecimal;

public interface CoveragePort {
    CoverageResult checkCoverage(String policyNumber, BigDecimal requestedAmount);
}
package com.seb4u.demo.claims.application.port;

import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.math.BigDecimal;

public interface PaymentReleasePort {
    PaymentReleaseResult requestPaymentRelease(ClaimId claimId, BigDecimal amount, String idempotencyKey);
}
package com.seb4u.demo.claims.application.port;

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

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

Code: ProcessClaimDecisionHandler

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

import com.seb4u.demo.claims.application.port.*;
import com.seb4u.demo.claims.domain.*;
import com.seb4u.demo.claims.sharedkernel.BusinessClock;

import java.util.Objects;

public final class ProcessClaimDecisionHandler {

    private final ClaimRepository claimRepository;
    private final DocumentVerificationPort documentVerificationPort;
    private final FraudRiskPort fraudRiskPort;
    private final CoveragePort coveragePort;
    private final PaymentReleasePort paymentReleasePort;
    private final CustomerMasterDataPort customerMasterDataPort;
    private final SecurityContextPort securityContextPort;
    private final AuditLogPort auditLogPort;
    private final OutboxPort outboxPort;
    private final ClaimTransitionPolicy transitionPolicy;
    private final AuthorizationPolicy authorizationPolicy;
    private final BusinessClock clock;

    public ProcessClaimDecisionHandler(ClaimRepository claimRepository,
                                       DocumentVerificationPort documentVerificationPort,
                                       FraudRiskPort fraudRiskPort,
                                       CoveragePort coveragePort,
                                       PaymentReleasePort paymentReleasePort,
                                       CustomerMasterDataPort customerMasterDataPort,
                                       SecurityContextPort securityContextPort,
                                       AuditLogPort auditLogPort,
                                       OutboxPort outboxPort,
                                       ClaimTransitionPolicy transitionPolicy,
                                       AuthorizationPolicy authorizationPolicy,
                                       BusinessClock clock) {
        this.claimRepository = Objects.requireNonNull(claimRepository);
        this.documentVerificationPort = Objects.requireNonNull(documentVerificationPort);
        this.fraudRiskPort = Objects.requireNonNull(fraudRiskPort);
        this.coveragePort = Objects.requireNonNull(coveragePort);
        this.paymentReleasePort = Objects.requireNonNull(paymentReleasePort);
        this.customerMasterDataPort = Objects.requireNonNull(customerMasterDataPort);
        this.securityContextPort = Objects.requireNonNull(securityContextPort);
        this.auditLogPort = Objects.requireNonNull(auditLogPort);
        this.outboxPort = Objects.requireNonNull(outboxPort);
        this.transitionPolicy = Objects.requireNonNull(transitionPolicy);
        this.authorizationPolicy = Objects.requireNonNull(authorizationPolicy);
        this.clock = Objects.requireNonNull(clock);
    }

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Objects.requireNonNull(command);

        UserSecurityContext userContext = securityContextPort.loadUserContext(command.actorUserId());
        authorizationPolicy.assertCanProcessDecision(userContext, command.decisionType());

        Claim claim = claimRepository.findById(command.claimId())
                .orElseThrow(() -> new ClaimNotFoundException(command.claimId()));

        CustomerSnapshot customer = customerMasterDataPort.loadCustomerSnapshot(command.customerNumber());

        DocumentVerificationResult documentResult = documentVerificationPort.verifyDocuments(
                command.claimId(),
                command.documentIds()
        );

        CoverageResult coverageResult = coveragePort.checkCoverage(
                command.policyNumber(),
                command.requestedAmount()
        );

        FraudRiskResult fraudRiskResult = fraudRiskPort.assessRisk(
                command.claimId(),
                command.customerNumber(),
                command.policyNumber()
        );

        DecisionPreconditionGuard.assertDecisionCanContinue(
                documentResult,
                coverageResult,
                fraudRiskResult,
                customer
        );

        ClaimDecision decision = new ClaimDecision(
                command.decisionType(),
                command.reasonCode(),
                command.comment(),
                command.actorUserId()
        );

        claim.applyDecision(decision, transitionPolicy, clock.now());
        claimRepository.save(claim);

        if (command.decisionType() == ClaimDecisionType.RELEASE_PAYMENT) {
            paymentReleasePort.requestPaymentRelease(
                    claim.id(),
                    claim.requestedAmount(),
                    command.idempotencyKey()
            );
        }

        auditLogPort.recordClaimDecision(claim, decision, userContext, clock.now());
        outboxPort.storeDomainEvents(claim.pullDomainEvents());

        return ProcessClaimDecisionResult.success(claim.id(), claim.status());
    }
}

Was ist daran besser als die Legacy-Methode?

Modul: claim-workflow

Zweck

claim-workflow enthÀlt explizite Workflow-Komponenten:

Warum eigenes Modul?

Im Legacy-System steckt Workflow hÀufig verstreut in:

Das Modul macht den Workflow sichtbar.

Code: Workflow Orchestrator

package com.seb4u.demo.claims.workflow;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.ClaimDecisionType;

import java.util.Objects;

public final class ClaimDecisionWorkflowOrchestrator {

    private final ProcessClaimDecisionHandler decisionHandler;
    private final ApprovalChain approvalChain;
    private final EscalationPolicy escalationPolicy;
    private final WorkflowAuditPort workflowAuditPort;

    public ClaimDecisionWorkflowOrchestrator(ProcessClaimDecisionHandler decisionHandler,
                                             ApprovalChain approvalChain,
                                             EscalationPolicy escalationPolicy,
                                             WorkflowAuditPort workflowAuditPort) {
        this.decisionHandler = Objects.requireNonNull(decisionHandler);
        this.approvalChain = Objects.requireNonNull(approvalChain);
        this.escalationPolicy = Objects.requireNonNull(escalationPolicy);
        this.workflowAuditPort = Objects.requireNonNull(workflowAuditPort);
    }

    public ProcessClaimDecisionResult process(ProcessClaimDecisionCommand command) {
        WorkflowPlan plan = buildWorkflowPlan(command);
        workflowAuditPort.recordPlan(command.claimId(), plan);

        if (plan.requiresApproval()) {
            approvalChain.requestApproval(command, plan);
            return ProcessClaimDecisionResult.pendingApproval(command.claimId());
        }

        if (plan.requiresEscalation()) {
            escalationPolicy.escalate(command.claimId(), plan.escalationReason());
            return ProcessClaimDecisionResult.escalated(command.claimId(), plan.escalationReason());
        }

        return decisionHandler.handle(command);
    }

    private WorkflowPlan buildWorkflowPlan(ProcessClaimDecisionCommand command) {
        if (command.decisionType() == ClaimDecisionType.APPROVE
                && command.requestedAmount().longValue() > 10000) {
            return WorkflowPlan.requiresApproval("Amount exceeds team approval limit");
        }

        if (command.decisionType() == ClaimDecisionType.ESCALATE) {
            return WorkflowPlan.requiresEscalation("Manual escalation requested by actor");
        }

        return WorkflowPlan.straightThrough();
    }
}

Modul: claim-document

Zweck

Dokumentenlogik war im Legacy-System besonders eng gekoppelt:

Modernes Ziel:

Code: DocumentStoragePort

package com.seb4u.demo.claims.document;

import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.io.InputStream;

public interface DocumentStoragePort {
    StoredDocument store(ClaimId claimId, DocumentUploadRequest request, InputStream content);
    RetrievedDocument retrieve(DocumentId documentId);
    void markAsArchived(DocumentId documentId, ArchiveReference archiveReference);
}

Code: FileShareDocumentAdapter

package com.seb4u.demo.claims.infrastructure.document;

import com.seb4u.demo.claims.document.*;
import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Objects;

public final class FileShareDocumentAdapter implements DocumentStoragePort {

    private final Path rootPath;
    private final DocumentMetadataRepository metadataRepository;

    public FileShareDocumentAdapter(Path rootPath,
                                    DocumentMetadataRepository metadataRepository) {
        this.rootPath = Objects.requireNonNull(rootPath);
        this.metadataRepository = Objects.requireNonNull(metadataRepository);
    }

    @Override
    public StoredDocument store(ClaimId claimId,
                                DocumentUploadRequest request,
                                InputStream content) {
        try {
            DocumentId documentId = DocumentId.newId();
            Path claimFolder = rootPath.resolve(claimId.value());
            Files.createDirectories(claimFolder);

            Path target = claimFolder.resolve(documentId.value() + "-" + sanitize(request.originalFileName()));
            Files.copy(content, target);

            StoredDocument document = new StoredDocument(
                    documentId,
                    claimId,
                    request.originalFileName(),
                    target.toString(),
                    Instant.now()
            );

            metadataRepository.save(document);
            return document;
        } catch (IOException ex) {
            throw new DocumentStorageException("Could not store document for claim " + claimId, ex);
        }
    }

    @Override
    public RetrievedDocument retrieve(DocumentId documentId) {
        StoredDocument metadata = metadataRepository.findById(documentId)
                .orElseThrow(() -> new DocumentNotFoundException(documentId));
        try {
            return new RetrievedDocument(metadata, Files.newInputStream(Path.of(metadata.storagePath())));
        } catch (IOException ex) {
            throw new DocumentStorageException("Could not retrieve document " + documentId, ex);
        }
    }

    @Override
    public void markAsArchived(DocumentId documentId, ArchiveReference archiveReference) {
        metadataRepository.markAsArchived(documentId, archiveReference);
    }

    private String sanitize(String originalFileName) {
        return originalFileName.replaceAll("[^a-zA-Z0-9._-]", "_");
    }
}

Modul: claim-payment

Zweck

Zahlungslogik wird aus dem Claim-Use-Case entkoppelt.

Legacy-Probleme:

Modernes Ziel:

Code: PaymentReleaseAdapter

package com.seb4u.demo.claims.infrastructure.payment;

import com.seb4u.demo.claims.application.port.PaymentReleasePort;
import com.seb4u.demo.claims.application.port.PaymentReleaseResult;
import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.math.BigDecimal;
import java.util.Objects;

public final class PaymentReleaseSoapAdapter implements PaymentReleasePort {

    private final PaymentReleaseSoapClient soapClient;
    private final PaymentIdempotencyRepository idempotencyRepository;

    public PaymentReleaseSoapAdapter(PaymentReleaseSoapClient soapClient,
                                     PaymentIdempotencyRepository idempotencyRepository) {
        this.soapClient = Objects.requireNonNull(soapClient);
        this.idempotencyRepository = Objects.requireNonNull(idempotencyRepository);
    }

    @Override
    public PaymentReleaseResult requestPaymentRelease(ClaimId claimId,
                                                      BigDecimal amount,
                                                      String idempotencyKey) {
        return idempotencyRepository.findExistingResult(idempotencyKey)
                .orElseGet(() -> requestAndStoreResult(claimId, amount, idempotencyKey));
    }

    private PaymentReleaseResult requestAndStoreResult(ClaimId claimId,
                                                       BigDecimal amount,
                                                       String idempotencyKey) {
        PaymentReleaseSoapRequest request = new PaymentReleaseSoapRequest(
                claimId.value(),
                amount,
                idempotencyKey
        );

        PaymentReleaseSoapResponse response = soapClient.releasePayment(request);

        PaymentReleaseResult result = new PaymentReleaseResult(
                response.paymentReference(),
                response.status(),
                response.message()
        );

        idempotencyRepository.store(idempotencyKey, result);
        return result;
    }
}

Modul: claim-audit

Zweck

Audit ist nicht nur Logging.

Audit muss beantworten:

Code: AuditLogPort

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

import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.domain.ClaimDecision;

import java.time.Instant;

public interface AuditLogPort {
    void recordClaimDecision(Claim claim,
                             ClaimDecision decision,
                             UserSecurityContext userContext,
                             Instant recordedAt);
}

Code: StructuredAuditLogAdapter

package com.seb4u.demo.claims.infrastructure.audit;

import com.seb4u.demo.claims.application.port.AuditLogPort;
import com.seb4u.demo.claims.application.port.UserSecurityContext;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.domain.ClaimDecision;

import java.time.Instant;
import java.util.Map;
import java.util.Objects;

public final class StructuredAuditLogAdapter implements AuditLogPort {

    private final AuditEventRepository auditEventRepository;

    public StructuredAuditLogAdapter(AuditEventRepository auditEventRepository) {
        this.auditEventRepository = Objects.requireNonNull(auditEventRepository);
    }

    @Override
    public void recordClaimDecision(Claim claim,
                                    ClaimDecision decision,
                                    UserSecurityContext userContext,
                                    Instant recordedAt) {
        AuditEventEntity event = new AuditEventEntity(
                "CLAIM_DECISION_RECORDED",
                claim.id().value(),
                userContext.userId(),
                userContext.roles(),
                recordedAt,
                Map.of(
                        "decisionType", decision.type().name(),
                        "reasonCode", decision.reasonCode(),
                        "claimStatus", claim.status().name()
                )
        );
        auditEventRepository.save(event);
    }
}

Modul: claim-search

Zweck

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

Legacy:

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

Modern:

Code: ClaimSearchPort

package com.seb4u.demo.claims.search;

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

Code: OracleClaimSearchAdapter

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

import com.seb4u.demo.claims.search.*;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class OracleClaimSearchAdapter implements ClaimSearchPort {

    private final DataSource dataSource;

    public OracleClaimSearchAdapter(DataSource dataSource) {
        this.dataSource = Objects.requireNonNull(dataSource);
    }

    @Override
    public ClaimSearchPage search(ClaimSearchCriteria criteria) {
        String sql = """
                SELECT CLAIM_ID, CLAIM_NUMBER, CUSTOMER_NUMBER, STATUS, UPDATED_AT
                FROM CLAIM_SEARCH_VIEW
                WHERE (? IS NULL OR CUSTOMER_NUMBER = ?)
                  AND (? IS NULL OR STATUS = ?)
                  AND (? IS NULL OR UPPER(CLAIM_NUMBER) LIKE UPPER(?))
                ORDER BY UPDATED_AT DESC
                FETCH FIRST ? ROWS ONLY
                """;

        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, criteria.customerNumber());
            statement.setString(2, criteria.customerNumber());
            statement.setString(3, criteria.status());
            statement.setString(4, criteria.status());
            statement.setString(5, criteria.freeText());
            statement.setString(6, criteria.freeText() == null ? null : "%" + criteria.freeText() + "%");
            statement.setInt(7, criteria.limit());

            try (ResultSet rs = statement.executeQuery()) {
                List<ClaimSearchHit> hits = new ArrayList<>();
                while (rs.next()) {
                    hits.add(new ClaimSearchHit(
                            rs.getString("CLAIM_ID"),
                            rs.getString("CLAIM_NUMBER"),
                            rs.getString("CUSTOMER_NUMBER"),
                            rs.getString("STATUS"),
                            rs.getTimestamp("UPDATED_AT").toInstant()
                    ));
                }
                return new ClaimSearchPage(hits, hits.size());
            }
        } catch (Exception ex) {
            throw new ClaimSearchException("Could not search claims", ex);
        }
    }
}

Modul: claim-notification

Zweck

Benachrichtigung wird aus EJB und JMS-Code getrennt.

Ziel:

Code: NotificationPort

package com.seb4u.demo.claims.notification;

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

Code: NotificationTemplatePolicy

package com.seb4u.demo.claims.notification;

import com.seb4u.demo.claims.domain.ClaimDecisionType;

public final class NotificationTemplatePolicy {

    public NotificationTemplate selectTemplate(ClaimDecisionType decisionType) {
        return switch (decisionType) {
            case APPROVE -> NotificationTemplate.of("claim-approved");
            case REJECT -> NotificationTemplate.of("claim-rejected");
            case REQUEST_MORE_DOCUMENTS -> NotificationTemplate.of("claim-documents-requested");
            case ESCALATE -> NotificationTemplate.of("claim-escalated");
            case SEND_TO_MANUAL_REVIEW -> NotificationTemplate.of("claim-manual-review");
            case RELEASE_PAYMENT -> NotificationTemplate.of("claim-payment-release");
        };
    }
}

Modul: claim-rest-api

Zweck

claim-rest-api bietet moderne REST-Endpunkte.

Wichtig:

REST ersetzt SOAP nicht sofort. In einer echten Migration bleiben SOAP und REST oft parallel bestehen.

REST-Ziele:

POM

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.seb4u.demo.claims</groupId>
    <artifactId>legacy-claims-customer-support-enterprise</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>claim-rest-api</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>claim-application</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  </dependencies>
</project>

Code: ClaimDecisionRestController

package com.seb4u.demo.claims.rest;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@RestController
@RequestMapping("/api/claims")
public class ClaimDecisionRestController {

    private final ProcessClaimDecisionHandler handler;

    public ClaimDecisionRestController(ProcessClaimDecisionHandler handler) {
        this.handler = handler;
    }

    @PostMapping("/{claimId}/decisions")
    public ResponseEntity<ClaimDecisionResponse> processDecision(
            @PathVariable String claimId,
            @RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
            @Valid @RequestBody ClaimDecisionRequest request) {

        ProcessClaimDecisionCommand command = new ProcessClaimDecisionCommand(
                ClaimId.of(claimId),
                request.customerNumber(),
                request.policyNumber(),
                request.decisionType(),
                request.requestedAmount(),
                request.reasonCode(),
                request.comment(),
                request.actorUserId(),
                request.documentIds(),
                idempotencyKey
        );

        ProcessClaimDecisionResult result = handler.handle(command);

        ClaimDecisionResponse response = ClaimDecisionResponse.from(result);
        return ResponseEntity.created(URI.create("/api/claims/" + claimId + "/decisions/latest"))
                .body(response);
    }
}

Code: ClaimDecisionRequest

package com.seb4u.demo.claims.rest;

import com.seb4u.demo.claims.domain.ClaimDecisionType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.math.BigDecimal;
import java.util.List;

public record ClaimDecisionRequest(
        @NotBlank String customerNumber,
        @NotBlank String policyNumber,
        @NotNull ClaimDecisionType decisionType,
        @Positive BigDecimal requestedAmount,
        String reasonCode,
        String comment,
        @NotBlank String actorUserId,
        List<String> documentIds
) {
}

Modul: claim-soap-api

Zweck

claim-soap-api hÀlt alte SOAP-Clients kompatibel.

Wichtig:

SOAP bleibt als Boundary bestehen, aber die Fachlogik ist nicht mehr in SOAP/EJB versteckt.

Beispiel: SOAP Boundary ruft modernen Use Case

package com.seb4u.demo.claims.soap;

import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionCommand;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionHandler;
import com.seb4u.demo.claims.application.decision.ProcessClaimDecisionResult;
import com.seb4u.demo.claims.domain.ClaimDecisionType;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebService;

@WebService(serviceName = "ClaimDecisionService")
public class ClaimDecisionSoapEndpoint {

    private final ProcessClaimDecisionHandler handler;

    public ClaimDecisionSoapEndpoint(ProcessClaimDecisionHandler handler) {
        this.handler = handler;
    }

    @WebMethod(operationName = "processClaimDecision")
    public ProcessClaimDecisionSoapResponse processClaimDecision(
            @WebParam(name = "request") ProcessClaimDecisionSoapRequest request) {

        ProcessClaimDecisionCommand command = new ProcessClaimDecisionCommand(
                ClaimId.of(request.getClaimId()),
                request.getCustomerNumber(),
                request.getPolicyNumber(),
                ClaimDecisionType.valueOf(request.getDecisionType()),
                request.getRequestedAmount(),
                request.getReasonCode(),
                request.getComment(),
                request.getActorUserId(),
                request.getDocumentIds(),
                request.getIdempotencyKey()
        );

        ProcessClaimDecisionResult result = handler.handle(command);

        return ProcessClaimDecisionSoapResponse.from(result);
    }
}

Migrationseffekt:

Modul: claim-openapi

Zweck

claim-openapi enthÀlt OpenAPI-Spezifikationen oder Generator-Konfiguration.

Beispielstruktur:

claim-openapi/
  pom.xml
  src/main/resources/openapi/claims-api.yaml
  src/main/resources/openapi/partner-portal-api.yaml
  src/main/resources/openapi/backoffice-api.yaml

Beispiel: claims-api.yaml

openapi: 3.0.3
info:
  title: Claims API
  version: 1.0.0
  description: Modern REST API for claim decision workflows.
paths:
  /api/claims/{claimId}/decisions:
    post:
      summary: Process claim decision
      operationId: processClaimDecision
      parameters:
        - name: claimId
          in: path
          required: true
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClaimDecisionRequest'
      responses:
        '201':
          description: Decision accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimDecisionResponse'
components:
  schemas:
    ClaimDecisionRequest:
      type: object
      required:
        - customerNumber
        - policyNumber
        - decisionType
        - requestedAmount
        - actorUserId
      properties:
        customerNumber:
          type: string
        policyNumber:
          type: string
        decisionType:
          type: string
          enum:
            - APPROVE
            - REJECT
            - REQUEST_MORE_DOCUMENTS
            - ESCALATE
            - SEND_TO_MANUAL_REVIEW
            - RELEASE_PAYMENT
        requestedAmount:
          type: number
        reasonCode:
          type: string
        comment:
          type: string
        actorUserId:
          type: string
        documentIds:
          type: array
          items:
            type: string
    ClaimDecisionResponse:
      type: object
      properties:
        claimId:
          type: string
        status:
          type: string
        message:
          type: string

Modul: claim-batch

Zweck

claim-batch ersetzt TimerBeans und Scheduler-Logik.

Legacy:

Modern:

Code: EscalationBatchJob

package com.seb4u.demo.claims.batch;

import com.seb4u.demo.claims.workflow.EscalationPolicy;

import java.time.Instant;
import java.util.List;
import java.util.Objects;

public final class EscalationBatchJob {

    private final BatchLeaseRepository leaseRepository;
    private final OpenClaimRepository openClaimRepository;
    private final EscalationPolicy escalationPolicy;
    private final BatchAuditPort batchAuditPort;

    public EscalationBatchJob(BatchLeaseRepository leaseRepository,
                              OpenClaimRepository openClaimRepository,
                              EscalationPolicy escalationPolicy,
                              BatchAuditPort batchAuditPort) {
        this.leaseRepository = Objects.requireNonNull(leaseRepository);
        this.openClaimRepository = Objects.requireNonNull(openClaimRepository);
        this.escalationPolicy = Objects.requireNonNull(escalationPolicy);
        this.batchAuditPort = Objects.requireNonNull(batchAuditPort);
    }

    public void run(Instant now) {
        if (!leaseRepository.tryAcquire("claim-escalation-job", now)) {
            batchAuditPort.recordSkipped("claim-escalation-job", "Lease already held", now);
            return;
        }

        try {
            List<OpenClaimProjection> claims = openClaimRepository.findOpenClaimsForEscalation(now);
            for (OpenClaimProjection claim : claims) {
                escalationPolicy.evaluateAndEscalate(claim, now);
            }
            batchAuditPort.recordSuccess("claim-escalation-job", claims.size(), now);
        } catch (Exception ex) {
            batchAuditPort.recordFailure("claim-escalation-job", ex, now);
            throw ex;
        } finally {
            leaseRepository.release("claim-escalation-job");
        }
    }
}

Modul: claim-infrastructure

Zweck

claim-infrastructure enthÀlt technische Implementierungen.

Beispiele:

claim-infrastructure/
  src/main/java/com/seb4u/demo/claims/infrastructure/
    persistence/
      JpaClaimRepository.java
      ClaimEntity.java
      ClaimMapper.java
    document/
      FileShareDocumentAdapter.java
      ArchiveDocumentAdapter.java
    fraud/
      FraudRiskSoapAdapter.java
    coverage/
      CoverageSoapAdapter.java
    payment/
      PaymentReleaseSoapAdapter.java
    notification/
      JmsNotificationAdapter.java
    audit/
      StructuredAuditLogAdapter.java
    search/
      OracleClaimSearchAdapter.java
    security/
      LdapSecurityContextAdapter.java
    outbox/
      JdbcOutboxPort.java
      OutboxPublisherWorker.java
    config/
      ClaimsRuntimeConfiguration.java

POM

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.seb4u.demo.claims</groupId>
    <artifactId>legacy-claims-customer-support-enterprise</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>claim-infrastructure</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>claim-application</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>com.seb4u.demo.claims</groupId>
      <artifactId>claim-document</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
  </dependencies>
</project>

Code: JpaClaimRepository

package com.seb4u.demo.claims.infrastructure.persistence;

import com.seb4u.demo.claims.application.port.ClaimRepository;
import com.seb4u.demo.claims.domain.Claim;
import com.seb4u.demo.claims.sharedkernel.ClaimId;

import java.util.Optional;

public final class JpaClaimRepository implements ClaimRepository {

    private final SpringDataClaimEntityRepository repository;
    private final ClaimEntityMapper mapper;

    public JpaClaimRepository(SpringDataClaimEntityRepository repository,
                              ClaimEntityMapper mapper) {
        this.repository = repository;
        this.mapper = mapper;
    }

    @Override
    public Optional<Claim> findById(ClaimId claimId) {
        return repository.findById(claimId.value()).map(mapper::toDomain);
    }

    @Override
    public void save(Claim claim) {
        ClaimEntity entity = mapper.toEntity(claim);
        repository.save(entity);
    }
}

Moderne Konfiguration statt JNDI-Wildwuchs

Legacy:

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

Modern:

package com.seb4u.demo.claims.infrastructure.config;

import java.nio.file.Path;

public record ClaimsRuntimeConfiguration(
        String databaseUrl,
        String databaseUser,
        String fraudRiskEndpoint,
        String coverageEndpoint,
        String paymentEndpoint,
        String notificationQueueName,
        Path documentRootPath,
        int soapTimeoutMillis,
        int retryAttempts
) {
}

In OpenShift kann diese Konfiguration spĂ€ter ĂŒber:

bereitgestellt werden.

Zusatzsysteme als moderne Adapter-Landschaft

ZusatzsystemModernes PortAdapterModul
Legacy Claims Partner PortalPartnerPortalUseCasePartnerPortalRestController / BFF Adapterclaim-rest-api
Internal Claims BackofficeClaimBackofficeUseCaseBackofficeRestControllerclaim-rest-api
Document Management AdapterDocumentStoragePortFileShareDocumentAdapterclaim-document / infrastructure
Fraud Risk GatewayFraudRiskPortFraudRiskSoapAdapterclaim-infrastructure
Policy Coverage SystemCoveragePortCoverageSoapAdapterclaim-infrastructure
Payment Release SystemPaymentReleasePortPaymentReleaseSoapAdapterclaim-payment / infrastructure
Notification SystemNotificationPortJmsNotificationAdapterclaim-notification / infrastructure
SLA & Escalation SchedulerEscalationPolicyEscalationBatchJobclaim-batch
Audit & Compliance ReporterAuditLogPortStructuredAuditLogAdapterclaim-audit
Search IndexerClaimSearchPortOracleClaimSearchAdapter / SearchIndexAdapterclaim-search
Customer Master Data SystemCustomerMasterDataPortCustomerMasterDataSoapAdapterclaim-infrastructure
Identity & Role ManagementSecurityContextPortLdapSecurityContextAdapterclaim-infrastructure

Moderne Security Boundary

Code: SecurityContextPort

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

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

Code: AuthorizationPolicy

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

import com.seb4u.demo.claims.domain.ClaimDecisionType;

public final class AuthorizationPolicy {

    public void assertCanProcessDecision(UserSecurityContext context,
                                         ClaimDecisionType decisionType) {
        if (decisionType == ClaimDecisionType.APPROVE && !context.hasRole("CLAIMS_APPROVER")) {
            throw new AuthorizationException("User is not allowed to approve claims");
        }
        if (decisionType == ClaimDecisionType.RELEASE_PAYMENT && !context.hasRole("PAYMENT_RELEASE_MANAGER")) {
            throw new AuthorizationException("User is not allowed to release payments");
        }
        if (decisionType == ClaimDecisionType.REJECT && !context.hasAnyRole("CLAIMS_AGENT", "CLAIMS_APPROVER")) {
            throw new AuthorizationException("User is not allowed to reject claims");
        }
    }
}

Code: LdapSecurityContextAdapter

package com.seb4u.demo.claims.infrastructure.security;

import com.seb4u.demo.claims.application.port.SecurityContextPort;
import com.seb4u.demo.claims.application.port.UserSecurityContext;

import java.util.Set;

public final class LdapSecurityContextAdapter implements SecurityContextPort {

    private final LdapGroupClient ldapGroupClient;
    private final RoleMapping roleMapping;

    public LdapSecurityContextAdapter(LdapGroupClient ldapGroupClient,
                                      RoleMapping roleMapping) {
        this.ldapGroupClient = ldapGroupClient;
        this.roleMapping = roleMapping;
    }

    @Override
    public UserSecurityContext loadUserContext(String userId) {
        Set<String> ldapGroups = ldapGroupClient.loadGroups(userId);
        Set<String> roles = roleMapping.mapGroupsToRoles(ldapGroups);
        return new UserSecurityContext(userId, roles);
    }
}

Outbox-Modul in Infrastructure

Warum Outbox?

Legacy-Problem:

  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

package com.seb4u.demo.claims.infrastructure.outbox;

import com.seb4u.demo.claims.application.port.OutboxPort;
import com.seb4u.demo.claims.domain.ClaimDomainEvent;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.UUID;

public final class JdbcOutboxPort implements OutboxPort {

    private final DataSource dataSource;
    private final DomainEventJsonMapper mapper;

    public JdbcOutboxPort(DataSource dataSource, DomainEventJsonMapper mapper) {
        this.dataSource = dataSource;
        this.mapper = mapper;
    }

    @Override
    public void storeDomainEvents(List<ClaimDomainEvent> events) {
        String sql = """
                INSERT INTO OUTBOX_EVENT
                (EVENT_ID, EVENT_TYPE, AGGREGATE_ID, PAYLOAD_JSON, STATUS, CREATED_AT)
                VALUES (?, ?, ?, ?, 'NEW', SYSTIMESTAMP)
                """;

        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {
            for (ClaimDomainEvent event : events) {
                statement.setString(1, UUID.randomUUID().toString());
                statement.setString(2, event.eventType());
                statement.setString(3, event.aggregateId());
                statement.setString(4, mapper.toJson(event));
                statement.addBatch();
            }
            statement.executeBatch();
        } catch (Exception ex) {
            throw new OutboxStorageException("Could not store outbox events", ex);
        }
    }
}

Tests im modernen Zielprojekt

Testpyramide

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

Domain Test

package com.seb4u.demo.claims.domain;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ClaimTransitionPolicyTest {

    private final ClaimTransitionPolicy policy = new ClaimTransitionPolicy();

    @Test
    void allowsApprovalForDocumentsReceived() {
        ClaimStatus next = policy.nextStatus(ClaimStatus.DOCUMENTS_RECEIVED, ClaimDecisionType.APPROVE);
        assertThat(next).isEqualTo(ClaimStatus.APPROVED);
    }

    @Test
    void rejectsPaymentReleaseForCreatedClaim() {
        assertThatThrownBy(() -> policy.nextStatus(ClaimStatus.CREATED, ClaimDecisionType.RELEASE_PAYMENT))
                .isInstanceOf(InvalidClaimTransitionException.class);
    }
}

Application Test mit Fake Ports

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

import com.seb4u.demo.claims.application.port.*;
import com.seb4u.demo.claims.domain.*;
import com.seb4u.demo.claims.sharedkernel.BusinessClock;
import com.seb4u.demo.claims.sharedkernel.ClaimId;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

class ProcessClaimDecisionHandlerTest {

    @Test
    void approvesClaimWhenDocumentsCoverageFraudAndAuthorizationAreValid() {
        ClaimId claimId = ClaimId.of("CLM-1001");
        InMemoryClaimRepository repository = new InMemoryClaimRepository();
        repository.save(new Claim(claimId, "C-77", BigDecimal.valueOf(900), ClaimStatus.DOCUMENTS_RECEIVED));

        ProcessClaimDecisionHandler handler = new ProcessClaimDecisionHandler(
                repository,
                (id, docs) -> DocumentVerificationResult.valid(),
                (id, customer, policy) -> FraudRiskResult.lowRisk(),
                (policy, amount) -> CoverageResult.covered(),
                (id, amount, key) -> PaymentReleaseResult.notRequested(),
                customer -> CustomerSnapshot.active(customer),
                user -> new UserSecurityContext(user, Set.of("CLAIMS_APPROVER")),
                new SpyAuditLogPort(),
                new InMemoryOutboxPort(),
                new ClaimTransitionPolicy(),
                new AuthorizationPolicy(),
                new BusinessClock(Clock.fixed(Instant.parse("2026-07-05T08:00:00Z"), ZoneOffset.UTC))
        );

        ProcessClaimDecisionResult result = handler.handle(new ProcessClaimDecisionCommand(
                claimId,
                "C-77",
                "P-300",
                ClaimDecisionType.APPROVE,
                BigDecimal.valueOf(900),
                "OK",
                "All documents valid",
                "agent-1",
                List.of("DOC-1"),
                "idem-1"
        ));

        assertThat(result.status()).isEqualTo(ClaimStatus.APPROVED);
        assertThat(repository.findById(claimId)).get().extracting(Claim::status).isEqualTo(ClaimStatus.APPROVED);
    }

    static final class InMemoryClaimRepository implements ClaimRepository {
        private final Map<ClaimId, Claim> claims = new HashMap<>();

        @Override
        public Optional<Claim> findById(ClaimId claimId) {
            return Optional.ofNullable(claims.get(claimId));
        }

        @Override
        public void save(Claim claim) {
            claims.put(claim.id(), claim);
        }
    }
}

Hinweis: In der finalen Beispielprojekt-Datei werden fehlende Imports ergÀnzt. Im Lerntext ist die Struktur wichtiger als Kompilierbarkeit jedes Ausschnitts.

Architekturtests mit ArchUnit

Ziel

Architekturtests verhindern, dass spÀter wieder Fachlogik in Adapter oder APIs wandert.

Code

package com.seb4u.demo.claims.architecture;

import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

class ArchitectureBoundaryTest {

    @Test
    void domainMustNotDependOnFrameworks() {
        var classes = new ClassFileImporter().importPackages("com.seb4u.demo.claims");

        ArchRule rule = noClasses()
                .that().resideInAPackage("..domain..")
                .should().dependOnClassesThat().resideInAnyPackage(
                        "org.springframework..",
                        "jakarta.persistence..",
                        "jakarta.jms..",
                        "jakarta.jws..",
                        "javax.sql..",
                        "java.sql.."
                );

        rule.check(classes);
    }

    @Test
    void applicationMustNotDependOnInfrastructure() {
        var classes = new ClassFileImporter().importPackages("com.seb4u.demo.claims");

        ArchRule rule = noClasses()
                .that().resideInAPackage("..application..")
                .should().dependOnClassesThat().resideInAPackage("..infrastructure..");

        rule.check(classes);
    }
}

Moderne Runtime-Varianten

Das Lernprojekt erlaubt mehrere Ziel-Runtimes:

Open Liberty / Jakarta

Geeignet, wenn:

Spring Boot

Geeignet, wenn:

Hybrid

Realistisch bei großen Migrationen:

Merksatz:

Die wichtigste Entscheidung ist nicht Spring oder Jakarta. Die wichtigste Entscheidung ist, dass Domain und Application nicht von beiden abhÀngig werden.

Moderne Deployment-Struktur vorbereitet fĂŒr OpenShift

Noch keine vollstÀndige OpenShift-Runde, aber die Zielstruktur wird vorbereitet:

migration_to_openshift/
  base/
    deployment.yaml
    service.yaml
    route.yaml
    configmap.yaml
    secret-template.yaml
  overlays/
    dev/
    test/
    prod/
  cronjobs/
    claim-escalation-cronjob.yaml
  docs/
    runtime-boundaries.md
    configuration-strategy.md
    secret-strategy.md

Diese Dateien werden in Runde 5 ausfĂŒhrlich erzeugt.

Before/After-Mapping aus Runde 4

Legacy-DateiModerne DateiGrundPattern
LegacyClaimFacadeBean.javaProcessClaimDecisionHandler.javaMonster Method zerlegtCommand Handler
LegacyClaimStatusIfElse.javaClaimTransitionPolicy.javaStatuslogik getrenntPolicy Object / State
LegacyDocumentShareClient.javaDocumentStoragePort + FileShareDocumentAdapterFileShare entkoppeltPort/Adapter
FraudRiskSoapClient.javaFraudRiskPort + FraudRiskSoapAdapterSOAP entkoppeltAnti-Corruption Layer
PolicyCoverageSoapClient.javaCoveragePort + CoverageSoapAdapterCoverage getrenntPort/Adapter
PaymentReleaseSoapClient.javaPaymentReleasePort + PaymentReleaseSoapAdapterZahlung entkoppeltIdempotency / Adapter
LegacyNotificationSenderBean.javaNotificationPort + NotificationAdapterNotification getrenntOutbox / Port
SlaEscalationTimerBean.javaEscalationBatchJob + EscalationPolicyTimerBean getrenntBatch Worker / Policy
AuditReportStoredProcedureDao.javaAuditLogPort + StructuredAuditLogAdapterAudit fachlich gemachtAudit Boundary
LegacyClaimSearchDao.javaClaimSearchPort + SearchIndexAdapterSuche getrenntRead Model / Adapter
CustomerMasterDataSoapClient.javaCustomerMasterDataPort + CustomerMasterDataSoapAdapterKundendaten entkoppeltACL
JaasRoleChecker.javaSecurityContextPort + AuthorizationPolicyRollenlogik getrenntSecurity Boundary

Changelog-V2-Vorbereitung aus Runde 4

Diese Runde liefert Stoff fĂŒr folgende Changelog-V2-EintrĂ€ge:

IDÄnderungKategorie
CHG-004-001Maven Parent und Module eingefĂŒhrtBuild / Struktur
CHG-004-002Domain-Modul frameworkfrei gemachtArchitektur
CHG-004-003Application-Modul mit Use Cases und Ports eingefĂŒhrtArchitektur
CHG-004-004Infrastructure-Modul fĂŒr Adapter eingefĂŒhrtIntegration
CHG-004-005SOAP Boundary kompatibel gehaltenMigration
CHG-004-006REST/OpenAPI ergÀnztModernisierung
CHG-004-007Claim Workflow sichtbar gemachtFacharchitektur
CHG-004-008Document Adapter getrenntIntegration
CHG-004-009Payment mit Idempotency vorbereitetIntegration
CHG-004-010Audit Trail strukturiertCompliance
CHG-004-011Search als Port modelliertPerformance / Entkopplung
CHG-004-012SecurityContextPort und AuthorizationPolicy eingefĂŒhrtSecurity
CHG-004-013Batch/SLA als Worker vorbereitetBetrieb
CHG-004-014Architekturtests ergÀnztQualitÀt

Risiken der Zielstruktur

Zu viele Module

Risiko:

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

Gegenmaßnahme:

Falscher Shared Kernel

Risiko:

shared-kernel wird zum MĂŒllplatz fĂŒr alles.

Gegenmaßnahme:

Ports ohne fachlichen Sinn

Risiko:

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

Gegenmaßnahme:

REST zu frĂŒh als Ersatz fĂŒr SOAP

Risiko:

Partner-Systeme brechen.

Gegenmaßnahme:

Infrastructure schleicht zurĂŒck in Application

Risiko:

Application-Code importiert wieder SOAP, JDBC oder JMS.

Gegenmaßnahme:

Ergebnis der Runde 4

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

Erzeugt wurden:

6. Migration nach OpenShift

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Migration nach OpenShift in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Projekt: Legacy Claims & Customer Support Enterprise System Runde: 05 Thema: Migration nach OpenShift Datum/Reihenfolge: Runde 05 nach Runde 04 — Modernes Zielprojekt mit modularer Maven-Struktur

Ziel dieser Runde

Diese Runde beschreibt die Migration des modernisierten Zielprojekts nach Red Hat OpenShift. Der Fokus liegt nicht nur auf „Dockerfile schreiben und deployen“, sondern auf einer realistischen Enterprise-Migration von einer alten WebSphere-Traditional-Landschaft in eine containerisierte Plattform.

Die Runde baut auf den vorherigen Runden auf:

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

Vorher war das System stark an WebSphere Traditional gebunden:

Legacy EAR
 ├─ WAR: Partner Portal JSP/Servlet
 ├─ WAR: Internal Backoffice JSP/Servlet
 ├─ EJB-JAR: Claim Facade, TimerBeans, MDBs
 ├─ SOAP Endpoints ĂŒber JAX-WS
 ├─ JNDI DataSources
 ├─ JAAS / LDAP Rollen
 ├─ IBM MQ JMS Resources
 ├─ Oracle Stored Procedures
 ├─ File Shares fĂŒr Dokumente
 └─ manuelle Deployment-/Config-Schritte in WebSphere Console

Nach der Migration soll die Plattform trennbarer und automatisierbarer sein:

OpenShift Namespace: claims-prod
 ├─ claim-service
 │   ├─ REST API
 │   ├─ SOAP Compatibility API
 │   ├─ Application Use Cases
 │   ├─ Domain Logic
 │   ├─ Infrastructure Adapters
 │   └─ Health / Metrics / Logs
 │
 ├─ claim-batch
 │   ├─ SLA Escalation Worker
 │   ├─ Outbox Publisher Worker
 │   └─ CronJob / Scheduled Job
 │
 ├─ claim-search-indexer
 │   ├─ Search Read Model Updater
 │   └─ Optional OpenSearch Adapter
 │
 ├─ Oracle / external DB connection
 ├─ IBM MQ / AMQ / external message broker
 ├─ Object Storage / File Share Adapter
 ├─ Identity Provider integration
 ├─ ConfigMaps / Secrets
 ├─ Routes / Services
 ├─ NetworkPolicies
 └─ Observability stack

Wichtig: Migration bedeutet nicht, dass alles sofort Microservice wird. FĂŒr dieses Lernprojekt ist eine kontrollierte modulare Monolith-zu-Service-Migration sinnvoller:

Migrationsstrategie

Empfohlene Strategie: Strangler + modulare Runtime

Die Migration erfolgt in Schritten:

  1. Legacy stabilisieren

Characterization Tests, Golden Master Tests, SOAP Contract Tests.

  1. Runtime-Seams schaffen

Fachlogik darf nicht mehr direkt von WebSphere APIs abhÀngig sein.

  1. Konfiguration externalisieren

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

  1. ContainerfÀhige Zielruntime bauen

Open Liberty oder Spring Boot/Jakarta Hybrid.

  1. SOAP-kompatible Fassade beibehalten

Partner und Altsysteme werden nicht sofort gebrochen.

  1. REST/OpenAPI ergÀnzen

Neue Clients nutzen moderne API, alte Clients nutzen SOAP weiter.

  1. Batch/Scheduler nach OpenShift CronJob verschieben

EJB Timer wird nicht 1:1 ĂŒbernommen.

  1. JMS/Outbox sauber machen

Events werden nicht mehr innerhalb der Monster Method blind publiziert.

  1. Security modernisieren

JAAS/WebSphere-Rollen werden in SecurityContextPort und AuthorizationPolicy ĂŒberfĂŒhrt.

  1. 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:

Nachteile:

Option B: Spring Boot als moderne Runtime

Vorteile:

Nachteile:

Empfehlung fĂŒr dieses Lernprojekt

FĂŒr das Lernprojekt wird ein Hybrid-Zielbild dokumentiert:

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

Ziel-Deployment-Struktur

migration_to_openshift/
 ├─ 01_container/
 │   ├─ Containerfile.openliberty
 │   ├─ Containerfile.springboot
 │   ├─ liberty-server.xml
 │   └─ jvm.options
 │
 ├─ 02_openshift_manifests/
 │   ├─ namespace.yaml
 │   ├─ serviceaccount.yaml
 │   ├─ configmap-claim-service.yaml
 │   ├─ secret-claim-service.yaml
 │   ├─ deployment-claim-service.yaml
 │   ├─ service-claim-service.yaml
 │   ├─ route-claim-service.yaml
 │   ├─ cronjob-escalation.yaml
 │   ├─ cronjob-outbox-publisher.yaml
 │   ├─ networkpolicy.yaml
 │   ├─ hpa.yaml
 │   └─ pdb.yaml
 │
 ├─ 03_runtime_config/
 │   ├─ application-openshift.yml
 │   ├─ claims-runtime-boundaries.md
 │   ├─ datasource-migration.md
 │   ├─ mq-migration.md
 │   ├─ file-storage-migration.md
 │   └─ security-migration.md
 │
 ├─ 04_observability/
 │   ├─ logging.md
 │   ├─ metrics.md
 │   ├─ tracing.md
 │   ├─ health-checks.md
 │   └─ alerting.md
 │
 ├─ 05_migration_tests/
 │   ├─ SoapCompatibilityIT.java
 │   ├─ RestApiSmokeIT.java
 │   ├─ OutboxPublisherIT.java
 │   ├─ OracleConnectivityIT.java
 │   ├─ SecurityRoleMappingIT.java
 │   └─ OpenShiftDeploymentSmokeTest.md
 │
 └─ 06_decisions/
     ├─ MIGRATION_DECISIONS.md
     ├─ RISKS.md
     ├─ ROLLBACK_PLAN.md
     └─ CUTOVER_PLAN.md

Containerisierung

Containerfile fĂŒr Open Liberty

#### migration_to_openshift/01_container/Containerfile.openliberty
FROM icr.io/appcafe/open-liberty:kernel-slim-java17-openj9-ubi

USER root
RUN mkdir -p /config/apps /opt/claims/config /opt/claims/logs \
    && chown -R 1001:0 /config /opt/claims \
    && chmod -R g=u /config /opt/claims
USER 1001

COPY --chown=1001:0 liberty-server.xml /config/server.xml
COPY --chown=1001:0 jvm.options /config/jvm.options
COPY --chown=1001:0 target/claim-service.war /config/apps/claim-service.war

ENV WLP_LOGGING_CONSOLE_FORMAT=json
ENV WLP_LOGGING_CONSOLE_LOGLEVEL=info
ENV CLAIMS_CONFIG_DIR=/opt/claims/config

EXPOSE 9080 9443

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -fsS http://localhost:9080/health/live || exit 1

ErklÀrung

Dieses Containerfile zeigt die Open-Liberty-Variante. Wichtig ist:

Liberty server.xml

<!-- migration_to_openshift/01_container/liberty-server.xml -->
<server description="Claims Customer Support Service">

    <featureManager>
        <feature>jakartaee-10.0</feature>
        <feature>mpHealth-4.0</feature>
        <feature>mpMetrics-5.0</feature>
        <feature>mpConfig-3.0</feature>
        <feature>mpOpenAPI-3.1</feature>
    </featureManager>

    <httpEndpoint id="defaultHttpEndpoint"
                  host="*"
                  httpPort="9080"
                  httpsPort="9443" />

    <webApplication id="claim-service"
                    location="claim-service.war"
                    contextRoot="/claims" />

    <variable name="oracleJdbcUrl" value="${env.ORACLE_JDBC_URL}" />
    <variable name="oracleUser" value="${env.ORACLE_USER}" />
    <variable name="oraclePassword" value="${env.ORACLE_PASSWORD}" />

    <dataSource id="ClaimsDataSource" jndiName="jdbc/ClaimsDS">
        <jdbcDriver libraryRef="OracleLib" />
        <properties.oracle URL="${oracleJdbcUrl}"
                           user="${oracleUser}"
                           password="${oraclePassword}" />
    </dataSource>

    <library id="OracleLib">
        <fileset dir="/opt/claims/jdbc" includes="ojdbc*.jar" />
    </library>

</server>

Migrationshinweis

Im Legacy-System war jdbc/ClaimsDS in der WebSphere-Konsole definiert. In der Zielwelt muss die DataSource reproduzierbar sein. Entweder:

Wichtig: Die Fachlogik darf den JNDI-Namen nicht kennen.

Containerfile fĂŒr Spring Boot Alternative

#### migration_to_openshift/01_container/Containerfile.springboot
FROM eclipse-temurin:17-jre-ubi9-minimal

WORKDIR /app

RUN mkdir -p /app/config /app/logs \
    && chown -R 1001:0 /app \
    && chmod -R g=u /app

USER 1001

COPY --chown=1001:0 target/claim-service.jar /app/claim-service.jar

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
ENV SPRING_PROFILES_ACTIVE=openshift

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/claim-service.jar"]

ErklÀrung

Die Spring-Boot-Variante wird als executable JAR betrieben. Diese Variante ist gut fĂŒr neue REST-/OpenAPI-Services. FĂŒr SOAP-KompatibilitĂ€t braucht sie aber bewusst gepflegte SOAP-Boundaries oder einen separaten SOAP-Adapter.

OpenShift Namespace und Basisobjekte

Namespace

#### migration_to_openshift/02_openshift_manifests/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: claims-prod
  labels:
    app.kubernetes.io/part-of: legacy-claims-customer-support
    environment: prod

ServiceAccount

#### migration_to_openshift/02_openshift_manifests/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: claim-service-sa
  namespace: claims-prod
  labels:
    app.kubernetes.io/name: claim-service

ConfigMap und Secret

ConfigMap

#### migration_to_openshift/02_openshift_manifests/configmap-claim-service.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: claim-service-config
  namespace: claims-prod
data:
  CLAIMS_ENVIRONMENT: "prod"
  CLAIMS_LOG_LEVEL: "INFO"
  CLAIMS_DEFAULT_LOCALE: "de_AT"
  CLAIMS_DOCUMENT_STORAGE_MODE: "file-share-adapter"
  CLAIMS_PAYMENT_IDEMPOTENCY_ENABLED: "true"
  CLAIMS_OUTBOX_ENABLED: "true"
  CLAIMS_SOAP_COMPATIBILITY_ENABLED: "true"
  CLAIMS_REST_API_ENABLED: "true"
  CLAIMS_SLA_ESCALATION_ENABLED: "true"
  CLAIMS_SEARCH_ADAPTER: "oracle-read-model"
  FRAUD_RISK_TIMEOUT_MS: "2500"
  COVERAGE_TIMEOUT_MS: "3000"
  PAYMENT_RELEASE_TIMEOUT_MS: "5000"
  NOTIFICATION_TIMEOUT_MS: "2000"

Secret

#### migration_to_openshift/02_openshift_manifests/secret-claim-service.yaml
apiVersion: v1
kind: Secret
metadata:
  name: claim-service-secret
  namespace: claims-prod
type: Opaque
stringData:
  ORACLE_JDBC_URL: "jdbc:oracle:thin:@//oracle.example.internal:1521/CLAIMS"
  ORACLE_USER: "claims_app"
  ORACLE_PASSWORD: "CHANGE_ME"
  IBM_MQ_HOST: "mq.example.internal"
  IBM_MQ_PORT: "1414"
  IBM_MQ_CHANNEL: "CLAIMS.SVRCONN"
  IBM_MQ_QUEUE_MANAGER: "CLAIMSQM"
  IBM_MQ_USER: "claims_mq"
  IBM_MQ_PASSWORD: "CHANGE_ME"
  LDAP_URL: "ldaps://ldap.example.internal:636"
  LDAP_BIND_DN: "cn=claims-service,ou=svc,dc=example,dc=internal"
  LDAP_BIND_PASSWORD: "CHANGE_ME"

Sicherheitsregel

In echten Projekten dĂŒrfen Secrets nicht im Git-Repository liegen. FĂŒr das Lernprojekt werden sie als Beispiel gezeigt, aber in der Praxis gehören sie in:

Claim Service Deployment

Deployment

#### migration_to_openshift/02_openshift_manifests/deployment-claim-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: claim-service
  namespace: claims-prod
  labels:
    app.kubernetes.io/name: claim-service
    app.kubernetes.io/part-of: legacy-claims-customer-support
spec:
  replicas: 2
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: claim-service
  template:
    metadata:
      labels:
        app.kubernetes.io/name: claim-service
        app.kubernetes.io/part-of: legacy-claims-customer-support
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: "/metrics"
        prometheus.io/port: "9080"
    spec:
      serviceAccountName: claim-service-sa
      containers:
        - name: claim-service
          image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-service:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 9080
            - name: https
              containerPort: 9443
          envFrom:
            - configMapRef:
                name: claim-service-config
            - secretRef:
                name: claim-service-secret
          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "1000m"
              memory: "1536Mi"
          readinessProbe:
            httpGet:
              path: /claims/health/ready
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /claims/health/live
              port: http
            initialDelaySeconds: 60
            periodSeconds: 20
            timeoutSeconds: 3
            failureThreshold: 3
          startupProbe:
            httpGet:
              path: /claims/health/started
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
            failureThreshold: 18
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: claims-runtime
              mountPath: /opt/claims/runtime
      volumes:
        - name: tmp
          emptyDir: {}
        - name: claims-runtime
          emptyDir: {}

ErklÀrung

Wichtige Migrationsentscheidungen:

Service und Route

Service

#### migration_to_openshift/02_openshift_manifests/service-claim-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: claim-service
  namespace: claims-prod
  labels:
    app.kubernetes.io/name: claim-service
spec:
  selector:
    app.kubernetes.io/name: claim-service
  ports:
    - name: http
      port: 80
      targetPort: 9080
    - name: https
      port: 443
      targetPort: 9443

Route

#### migration_to_openshift/02_openshift_manifests/route-claim-service.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: claim-service
  namespace: claims-prod
  labels:
    app.kubernetes.io/name: claim-service
spec:
  host: claims.example.at
  to:
    kind: Service
    name: claim-service
  port:
    targetPort: http
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect

SOAP- und REST-Pfade

Die Route kann beide Welten bedienen:

SOAP altkompatibel:
https://claims.example.at/claims/soap/ClaimService

REST neu:
https://claims.example.at/claims/api/v1/claims/{claimId}/decision

OpenAPI:
https://claims.example.at/claims/openapi

Health:
https://claims.example.at/claims/health/ready

Health Checks im Code

Health Resource

package com.seb4u.demo.claims.infrastructure.health;

import com.seb4u.demo.claims.application.port.out.ClaimRepository;
import com.seb4u.demo.claims.application.port.out.OutboxPort;
import com.seb4u.demo.claims.application.port.out.PaymentReleasePort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;

@ApplicationScoped
@Liveness
public class ClaimServiceLivenessCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("claim-service-live")
                .up()
                .withData("runtime", "openliberty")
                .withData("service", "claim-service")
                .build();
    }
}

@ApplicationScoped
@Readiness
class ClaimServiceReadinessCheck implements HealthCheck {

    private final ClaimRepository claimRepository;
    private final OutboxPort outboxPort;
    private final PaymentReleasePort paymentReleasePort;

    @Inject
    ClaimServiceReadinessCheck(
            ClaimRepository claimRepository,
            OutboxPort outboxPort,
            PaymentReleasePort paymentReleasePort
    ) {
        this.claimRepository = claimRepository;
        this.outboxPort = outboxPort;
        this.paymentReleasePort = paymentReleasePort;
    }

    @Override
    public HealthCheckResponse call() {
        HealthCheckResponse.Builder response = HealthCheckResponse.named("claim-service-ready");

        boolean repositoryReady = claimRepository.canConnect();
        boolean outboxReady = outboxPort.canPersistEvents();
        boolean paymentPortConfigured = paymentReleasePort.isConfigured();

        boolean ready = repositoryReady && outboxReady && paymentPortConfigured;

        response.status(ready)
                .withData("oracle", repositoryReady)
                .withData("outbox", outboxReady)
                .withData("paymentPortConfigured", paymentPortConfigured);

        return response.build();
    }
}

Warum so?

Liveness darf nicht zu viele externe Systeme prĂŒfen. Sonst wĂŒrde ein externer Ausfall den Pod unnötig neu starten. Readiness darf externe AbhĂ€ngigkeiten prĂŒfen, weil der Pod keinen Traffic bekommen soll, wenn er fachlich nicht arbeitsfĂ€hig ist.

Externe Konfiguration statt WebSphere JNDI

Legacy vorher

package com.seb4u.demo.claims.legacy.infrastructure;

import javax.naming.InitialContext;
import javax.sql.DataSource;

public class LegacyDataSourceLookup {

    public DataSource lookupClaimsDataSource() {
        try {
            InitialContext ctx = new InitialContext();
            return (DataSource) ctx.lookup("jdbc/claimsLegacyDS");
        } catch (Exception e) {
            throw new IllegalStateException("Could not lookup WebSphere DataSource", e);
        }
    }
}

Problem:

Nachher: Configuration Object

package com.seb4u.demo.claims.infrastructure.config;

import java.time.Duration;
import java.util.Objects;

public final class ClaimsRuntimeConfiguration {

    private final String oracleJdbcUrl;
    private final String oracleUser;
    private final String oraclePassword;
    private final Duration fraudRiskTimeout;
    private final Duration coverageTimeout;
    private final Duration paymentTimeout;
    private final boolean outboxEnabled;
    private final boolean soapCompatibilityEnabled;

    public ClaimsRuntimeConfiguration(
            String oracleJdbcUrl,
            String oracleUser,
            String oraclePassword,
            Duration fraudRiskTimeout,
            Duration coverageTimeout,
            Duration paymentTimeout,
            boolean outboxEnabled,
            boolean soapCompatibilityEnabled
    ) {
        this.oracleJdbcUrl = requireNonBlank(oracleJdbcUrl, "oracleJdbcUrl");
        this.oracleUser = requireNonBlank(oracleUser, "oracleUser");
        this.oraclePassword = requireNonBlank(oraclePassword, "oraclePassword");
        this.fraudRiskTimeout = Objects.requireNonNull(fraudRiskTimeout);
        this.coverageTimeout = Objects.requireNonNull(coverageTimeout);
        this.paymentTimeout = Objects.requireNonNull(paymentTimeout);
        this.outboxEnabled = outboxEnabled;
        this.soapCompatibilityEnabled = soapCompatibilityEnabled;
    }

    public String oracleJdbcUrl() {
        return oracleJdbcUrl;
    }

    public String oracleUser() {
        return oracleUser;
    }

    public String oraclePassword() {
        return oraclePassword;
    }

    public Duration fraudRiskTimeout() {
        return fraudRiskTimeout;
    }

    public Duration coverageTimeout() {
        return coverageTimeout;
    }

    public Duration paymentTimeout() {
        return paymentTimeout;
    }

    public boolean outboxEnabled() {
        return outboxEnabled;
    }

    public boolean soapCompatibilityEnabled() {
        return soapCompatibilityEnabled;
    }

    private static String requireNonBlank(String value, String name) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(name + " must not be blank");
        }
        return value;
    }
}

Vorteil

Datenbankmigration

Ausgangslage

Legacy nutzt:

Zielbild

Die Migration trennt:

Fachliches Modell
 ├─ Claim Aggregate
 ├─ Claim Status
 ├─ Claim Decision
 ├─ Claim Audit Trail
 └─ Claim Outbox Event

Persistenzmodell
 ├─ CLAIM
 ├─ CLAIM_DECISION
 ├─ CLAIM_DOCUMENT
 ├─ CLAIM_AUDIT_EVENT
 ├─ CLAIM_OUTBOX
 └─ CLAIM_SEARCH_READ_MODEL

Legacy-KompatibilitÀt
 ├─ Legacy Views
 ├─ Übergangs-Stored-Procedures
 └─ Vergleichstests gegen Golden Master

Migration mit Flyway/Liquibase als Konzept

-- migration_to_openshift/03_runtime_config/db/V001__create_claim_outbox.sql
CREATE TABLE CLAIM_OUTBOX (
    OUTBOX_ID VARCHAR2(64) PRIMARY KEY,
    AGGREGATE_ID VARCHAR2(64) NOT NULL,
    AGGREGATE_TYPE VARCHAR2(64) NOT NULL,
    EVENT_TYPE VARCHAR2(128) NOT NULL,
    EVENT_PAYLOAD CLOB NOT NULL,
    IDEMPOTENCY_KEY VARCHAR2(128) NOT NULL,
    STATUS VARCHAR2(32) NOT NULL,
    CREATED_AT TIMESTAMP NOT NULL,
    PUBLISHED_AT TIMESTAMP NULL,
    RETRY_COUNT NUMBER(10) DEFAULT 0 NOT NULL,
    LAST_ERROR VARCHAR2(2000) NULL
);

CREATE UNIQUE INDEX UX_CLAIM_OUTBOX_IDEMPOTENCY
    ON CLAIM_OUTBOX (IDEMPOTENCY_KEY);
-- migration_to_openshift/03_runtime_config/db/V002__create_claim_audit_event.sql
CREATE TABLE CLAIM_AUDIT_EVENT (
    AUDIT_EVENT_ID VARCHAR2(64) PRIMARY KEY,
    CLAIM_ID VARCHAR2(64) NOT NULL,
    ACTOR_ID VARCHAR2(128) NOT NULL,
    ACTOR_ROLE VARCHAR2(128) NOT NULL,
    ACTION VARCHAR2(128) NOT NULL,
    BUSINESS_REASON VARCHAR2(1000),
    CREATED_AT TIMESTAMP NOT NULL,
    CORRELATION_ID VARCHAR2(128) NOT NULL
);

CREATE INDEX IX_CLAIM_AUDIT_EVENT_CLAIM
    ON CLAIM_AUDIT_EVENT (CLAIM_ID, CREATED_AT);

Datenbank-Migrationsregel

Stored Procedures werden nicht blind gelöscht. FĂŒr jede Procedure gibt es eine Entscheidung:

Legacy Stored ProcedureZielStrategie
SP_PROCESS_CLAIM_DECISIONApplication Handler + Domain PoliciesSchrittweise ersetzen, Golden Master vergleichen
SP_REFRESH_CLAIM_REPORTINGReporting Read ModelÜbergangsweise behalten, spĂ€ter Read Model Worker
SP_ESCALATE_OVERDUE_CLAIMSEscalationBatchJob + EscalationPolicyLogik aus DB herausziehen
SP_EXPORT_AUDIT_CSVAuditReportingAdapterReporting Boundary schaffen

JMS / IBM MQ Migration

Legacy vorher

package com.seb4u.demo.claims.legacy.messaging;

import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.JMSContext;
import javax.jms.Queue;

@Stateless
public class LegacyClaimEventPublisher {

    @Resource(lookup = "jms/ClaimsEventQueue")
    private Queue claimsEventQueue;

    @Resource
    private JMSContext jmsContext;

    public void publishDecisionApproved(String claimId, String paymentId) {
        jmsContext.createProducer()
                .setProperty("claimId", claimId)
                .setProperty("paymentId", paymentId)
                .send(claimsEventQueue, "CLAIM_APPROVED:" + claimId + ":" + paymentId);
    }
}

Problem:

Nachher: Outbox Port

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

import com.seb4u.demo.claims.shared.kernel.CorrelationId;
import java.time.Instant;

public interface OutboxPort {

    void append(OutboxMessage message);

    boolean canPersistEvents();

    record OutboxMessage(
            String outboxId,
            String aggregateId,
            String aggregateType,
            String eventType,
            String payloadJson,
            String idempotencyKey,
            CorrelationId correlationId,
            Instant createdAt
    ) {
    }
}
package com.seb4u.demo.claims.infrastructure.outbox;

import com.seb4u.demo.claims.application.port.out.OutboxPort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;

@ApplicationScoped
public class JdbcOutboxAdapter implements OutboxPort {

    private final DataSource dataSource;

    @Inject
    public JdbcOutboxAdapter(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void append(OutboxMessage message) {
        String sql = """
                INSERT INTO CLAIM_OUTBOX
                (OUTBOX_ID, AGGREGATE_ID, AGGREGATE_TYPE, EVENT_TYPE, EVENT_PAYLOAD,
                 IDEMPOTENCY_KEY, STATUS, CREATED_AT)
                VALUES (?, ?, ?, ?, ?, ?, 'NEW', ?)
                """;

        try (Connection connection = dataSource.getConnection();
             PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, message.outboxId());
            ps.setString(2, message.aggregateId());
            ps.setString(3, message.aggregateType());
            ps.setString(4, message.eventType());
            ps.setString(5, message.payloadJson());
            ps.setString(6, message.idempotencyKey());
            ps.setObject(7, message.createdAt());
            ps.executeUpdate();
        } catch (Exception e) {
            throw new OutboxPersistenceException("Could not append outbox message", e);
        }
    }

    @Override
    public boolean canPersistEvents() {
        try (Connection connection = dataSource.getConnection()) {
            return connection.isValid(2);
        } catch (Exception e) {
            return false;
        }
    }
}

Outbox Publisher als OpenShift CronJob

#### migration_to_openshift/02_openshift_manifests/cronjob-outbox-publisher.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: claim-outbox-publisher
  namespace: claims-prod
spec:
  schedule: "*/2 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        metadata:
          labels:
            app.kubernetes.io/name: claim-outbox-publisher
        spec:
          restartPolicy: Never
          serviceAccountName: claim-service-sa
          containers:
            - name: outbox-publisher
              image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-batch:1.0.0
              args:
                - "run"
                - "outbox-publisher"
              envFrom:
                - configMapRef:
                    name: claim-service-config
                - secretRef:
                    name: claim-service-secret
              resources:
                requests:
                  cpu: "100m"
                  memory: "256Mi"
                limits:
                  cpu: "500m"
                  memory: "768Mi"

SLA & Escalation Scheduler Migration

Legacy vorher

package com.seb4u.demo.claims.legacy.scheduler;

import javax.ejb.Schedule;
import javax.ejb.Singleton;

@Singleton
public class SlaEscalationTimerBean {

    @Schedule(hour = "*/1", persistent = false)
    public void checkOverdueClaims() {
        // Oracle Query
        // Stored Procedure
        // direkte StatusÀnderung
        // direkte JMS-Benachrichtigung
        // direkte Teamleiter-Eskalation
    }
}

Problem:

Nachher: EscalationBatchJob

package com.seb4u.demo.claims.batch.escalation;

import com.seb4u.demo.claims.application.port.out.ClaimRepository;
import com.seb4u.demo.claims.application.port.out.NotificationPort;
import com.seb4u.demo.claims.application.port.out.OutboxPort;
import com.seb4u.demo.claims.workflow.escalation.EscalationPolicy;
import java.time.Clock;
import java.time.Instant;
import java.util.List;

public class EscalationBatchJob {

    private final ClaimRepository claimRepository;
    private final EscalationPolicy escalationPolicy;
    private final OutboxPort outboxPort;
    private final NotificationPort notificationPort;
    private final Clock clock;

    public EscalationBatchJob(
            ClaimRepository claimRepository,
            EscalationPolicy escalationPolicy,
            OutboxPort outboxPort,
            NotificationPort notificationPort,
            Clock clock
    ) {
        this.claimRepository = claimRepository;
        this.escalationPolicy = escalationPolicy;
        this.outboxPort = outboxPort;
        this.notificationPort = notificationPort;
        this.clock = clock;
    }

    public BatchResult run(BatchExecutionContext context) {
        Instant now = clock.instant();
        List<OpenClaimProjection> openClaims = claimRepository.findOpenClaimsForEscalation(context.partitionSize());

        int escalated = 0;
        int skipped = 0;

        for (OpenClaimProjection projection : openClaims) {
            EscalationDecision decision = escalationPolicy.evaluate(projection, now);

            if (decision.shouldEscalate()) {
                claimRepository.markEscalated(projection.claimId(), decision.reason(), context.executionId());
                outboxPort.append(decision.toOutboxMessage(context.correlationId()));
                notificationPort.prepareEscalationNotification(projection.claimId(), decision.reason());
                escalated++;
            } else {
                skipped++;
            }
        }

        return new BatchResult(context.executionId(), openClaims.size(), escalated, skipped);
    }
}

OpenShift CronJob

#### migration_to_openshift/02_openshift_manifests/cronjob-escalation.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: claim-sla-escalation
  namespace: claims-prod
spec:
  schedule: "0 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        metadata:
          labels:
            app.kubernetes.io/name: claim-sla-escalation
        spec:
          restartPolicy: Never
          serviceAccountName: claim-service-sa
          containers:
            - name: escalation-job
              image: image-registry.openshift-image-registry.svc:5000/claims-prod/claim-batch:1.0.0
              args:
                - "run"
                - "sla-escalation"
                - "--partition-size=500"
              envFrom:
                - configMapRef:
                    name: claim-service-config
                - secretRef:
                    name: claim-service-secret
              resources:
                requests:
                  cpu: "100m"
                  memory: "256Mi"
                limits:
                  cpu: "750m"
                  memory: "1Gi"

Wichtig

FĂŒr echte Produktion ist ein zusĂ€tzliches Lease-Lock sinnvoll, falls mehrere Jobs oder manuelle Starts möglich sind.

Lease Lock gegen doppelte BatchausfĂŒhrung

CREATE TABLE CLAIM_BATCH_LOCK (
    LOCK_NAME VARCHAR2(128) PRIMARY KEY,
    LOCK_OWNER VARCHAR2(128) NOT NULL,
    LOCK_UNTIL TIMESTAMP NOT NULL,
    UPDATED_AT TIMESTAMP NOT NULL
);
package com.seb4u.demo.claims.batch.lock;

import java.time.Duration;
import java.time.Instant;

public interface BatchLeaseLockPort {

    boolean tryAcquire(String lockName, String owner, Duration leaseDuration, Instant now);

    void release(String lockName, String owner);
}
package com.seb4u.demo.claims.batch.lock;

public final class LockedBatchRunner {

    private final BatchLeaseLockPort lockPort;

    public LockedBatchRunner(BatchLeaseLockPort lockPort) {
        this.lockPort = lockPort;
    }

    public BatchResult runWithLock(String lockName, String owner, BatchExecutable executable) {
        boolean acquired = lockPort.tryAcquire(lockName, owner, java.time.Duration.ofMinutes(30), java.time.Instant.now());
        if (!acquired) {
            return BatchResult.skipped("Lock not acquired: " + lockName);
        }

        try {
            return executable.run();
        } finally {
            lockPort.release(lockName, owner);
        }
    }
}

Document Storage Migration

Legacy Problem

Das Legacy-System verwendet:

Beispiel:

String path = "//legacy-share/claims/" + claimId + "/" + uploadedFileName;
Files.copy(inputStream, Path.of(path));

Problem:

Zielbild

DocumentStoragePort
 ├─ FileShareDocumentAdapter       Übergangsadapter
 ├─ ArchiveDocumentAdapter         Legacy Archive Service
 ├─ ObjectStorageDocumentAdapter   spĂ€ter S3/NooBaa/MinIO
 └─ DocumentVerificationPort       fachliche PrĂŒfung separat

OpenShift-Konfiguration fĂŒr Übergangs-FileShare

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claims-document-share-pvc
  namespace: claims-prod
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  storageClassName: managed-nfs-storage
### Ausschnitt im Deployment
volumeMounts:
  - name: document-share
    mountPath: /mnt/claims-documents
volumes:
  - name: document-share
    persistentVolumeClaim:
      claimName: claims-document-share-pvc

Migrationsentscheidung

Der FileShare-Adapter ist nur eine Übergangsstrategie. Die langfristige Richtung ist Object Storage.

Security Migration

Legacy vorher

package com.seb4u.demo.claims.legacy.security;

import javax.ejb.SessionContext;

public class JaasRoleChecker {

    private final SessionContext sessionContext;

    public JaasRoleChecker(SessionContext sessionContext) {
        this.sessionContext = sessionContext;
    }

    public boolean canApprovePayment() {
        return sessionContext.isCallerInRole("CLAIMS_MANAGER")

                || sessionContext.isCallerInRole("PAYMENT_APPROVER");
    }
}

Problem:

Nachher: SecurityContextPort + AuthorizationPolicy

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

import java.util.Set;

public interface SecurityContextPort {

    AuthenticatedActor currentActor();

    record AuthenticatedActor(
            String actorId,
            String displayName,
            Set<String> technicalRoles,
            Set<String> businessPermissions,
            String authenticationSource
    ) {
        public boolean hasPermission(String permission) {
            return businessPermissions.contains(permission);
        }
    }
}
package com.seb4u.demo.claims.application.security;

import com.seb4u.demo.claims.application.port.out.SecurityContextPort;
import com.seb4u.demo.claims.domain.claim.Claim;

public class AuthorizationPolicy {

    public void requireDecisionPermission(
            SecurityContextPort.AuthenticatedActor actor,
            Claim claim,
            String requestedDecision
    ) {
        if ("APPROVE_PAYMENT".equals(requestedDecision)
                && !actor.hasPermission("claim.payment.approve")) {
            throw new AccessDeniedBusinessException(
                    "Actor " + actor.actorId() + " is not allowed to approve claim payment");
        }

        if (claim.isPartnerSubmitted()
                && !actor.hasPermission("claim.partner.process")) {
            throw new AccessDeniedBusinessException(
                    "Actor " + actor.actorId() + " is not allowed to process partner submitted claims");
        }
    }
}

Role Mapping

#### migration_to_openshift/03_runtime_config/security-role-mapping.yaml
roleMappings:
  CLAIMS_AGENT:
    - claim.read
    - claim.document.review
    - claim.decision.prepare
  CLAIMS_MANAGER:
    - claim.read
    - claim.document.review
    - claim.decision.prepare
    - claim.payment.approve
    - claim.escalation.resolve
  PARTNER_USER:
    - claim.partner.create
    - claim.partner.uploadDocument
    - claim.partner.readOwnClaim
  COMPLIANCE_AUDITOR:
    - claim.audit.read
    - claim.report.export

NetworkPolicy

#### migration_to_openshift/02_openshift_manifests/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: claim-service-network-policy
  namespace: claims-prod
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: claim-service
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              network.openshift.io/policy-group: ingress
      ports:
        - protocol: TCP
          port: 9080
  egress:
    - to:
        - ipBlock:
            cidr: 10.20.0.0/16
      ports:
        - protocol: TCP
          port: 1521
    - to:
        - ipBlock:
            cidr: 10.30.0.0/16
      ports:
        - protocol: TCP
          port: 1414
    - to:
        - ipBlock:
            cidr: 10.40.0.0/16
      ports:
        - protocol: TCP
          port: 443

ErklÀrung

OpenShift-Migration ist nicht nur Anwendungsmigration. NetzwerkflĂŒsse mĂŒssen explizit werden:

Ressourcen, HPA und PDB

Horizontal Pod Autoscaler

#### migration_to_openshift/02_openshift_manifests/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: claim-service-hpa
  namespace: claims-prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: claim-service
  minReplicas: 2
  maxReplicas: 6
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

PodDisruptionBudget

#### migration_to_openshift/02_openshift_manifests/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: claim-service-pdb
  namespace: claims-prod
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: claim-service

Observability

Logging

Ziel:

package com.seb4u.demo.claims.infrastructure.logging;

import com.seb4u.demo.claims.shared.kernel.CorrelationId;

public final class StructuredLogEvent {

    private final String eventType;
    private final String claimId;
    private final CorrelationId correlationId;
    private final String message;

    public StructuredLogEvent(String eventType, String claimId, CorrelationId correlationId, String message) {
        this.eventType = eventType;
        this.claimId = claimId;
        this.correlationId = correlationId;
        this.message = message;
    }

    public String toJsonLine() {
        return "{"
                + "\"eventType\":\"" + escape(eventType) + "\","
                + "\"claimId\":\"" + escape(claimId) + "\","
                + "\"correlationId\":\"" + escape(correlationId.value()) + "\","
                + "\"message\":\"" + escape(message) + "\""
                + "}";
    }

    private String escape(String value) {
        return value == null ? "" : value.replace("\"", "\\\"");
    }
}

Metrics

Wichtige Metriken:

claims_decision_total{decision="APPROVED"}
claims_decision_total{decision="REJECTED"}
claims_payment_release_total{result="SUCCESS"}
claims_payment_release_total{result="FAILED"}
claims_fraud_risk_timeout_total
claims_document_verification_failed_total
claims_outbox_pending_total
claims_outbox_publish_failed_total
claims_sla_escalated_total
claims_soap_request_total
claims_rest_request_total

Tracing

Wichtige Trace-Spans:

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

REST Smoke Test

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

import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

class RestApiSmokeIT {

    @Test
    void shouldExposeHealthAndOpenApi() throws Exception {
        String baseUrl = System.getProperty("claims.baseUrl", "http://localhost:9080/claims");
        HttpClient client = HttpClient.newHttpClient();

        HttpResponse<String> health = client.send(
                HttpRequest.newBuilder(URI.create(baseUrl + "/health/ready")).GET().build(),
                HttpResponse.BodyHandlers.ofString()
        );

        assertThat(health.statusCode()).isBetween(200, 299);

        HttpResponse<String> openapi = client.send(
                HttpRequest.newBuilder(URI.create(baseUrl + "/openapi")).GET().build(),
                HttpResponse.BodyHandlers.ofString()
        );

        assertThat(openapi.statusCode()).isBetween(200, 299);
        assertThat(openapi.body()).contains("Process claim decision");
    }
}

SOAP Compatibility Test

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

import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

class SoapCompatibilityIT {

    @Test
    void shouldKeepProcessClaimDecisionSoapOperationCompatible() throws Exception {
        String baseUrl = System.getProperty("claims.baseUrl", "http://localhost:9080/claims");

        String soapEnvelope = """
                <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                                  xmlns:cla="http://seb4u.com/demo/claims">
                  <soapenv:Header/>
                  <soapenv:Body>
                    <cla:processClaimDecision>
                      <claimId>CLM-10001</claimId>
                      <decision>APPROVE_PAYMENT</decision>
                      <actorId>agent-42</actorId>
                      <reason>Coverage valid and documents verified</reason>
                    </cla:processClaimDecision>
                  </soapenv:Body>
                </soapenv:Envelope>
                """;

        HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/soap/ClaimService"))
                .header("Content-Type", "text/xml; charset=utf-8")
                .POST(HttpRequest.BodyPublishers.ofString(soapEnvelope))
                .build();

        HttpResponse<String> response = HttpClient.newHttpClient().send(
                request,
                HttpResponse.BodyHandlers.ofString()
        );

        assertThat(response.statusCode()).isBetween(200, 299);
        assertThat(response.body()).contains("processClaimDecisionResponse");
    }
}

Oracle Connectivity Test

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

import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;

import static org.assertj.core.api.Assertions.assertThat;

class OracleConnectivityIT {

    @Test
    void shouldConnectToOracleAndReadSchemaVersion() throws Exception {
        String url = System.getenv("ORACLE_JDBC_URL");
        String user = System.getenv("ORACLE_USER");
        String password = System.getenv("ORACLE_PASSWORD");

        try (Connection connection = DriverManager.getConnection(url, user, password);
             ResultSet rs = connection.createStatement().executeQuery("SELECT 1 FROM DUAL")) {
            assertThat(rs.next()).isTrue();
            assertThat(rs.getInt(1)).isEqualTo(1);
        }
    }
}

Security Role Mapping Test

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

import com.seb4u.demo.claims.application.port.out.SecurityContextPort.AuthenticatedActor;
import com.seb4u.demo.claims.application.security.AuthorizationPolicy;
import com.seb4u.demo.claims.domain.claim.Claim;
import org.junit.jupiter.api.Test;

import java.util.Set;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SecurityRoleMappingIT {

    @Test
    void partnerUserMustNotApprovePayment() {
        AuthenticatedActor partner = new AuthenticatedActor(
                "partner-17",
                "Partner User",
                Set.of("PARTNER_USER"),
                Set.of("claim.partner.create", "claim.partner.readOwnClaim"),
                "openshift-idp"
        );

        Claim claim = Claim.partnerSubmitted("CLM-4711");
        AuthorizationPolicy policy = new AuthorizationPolicy();

        assertThatThrownBy(() -> policy.requireDecisionPermission(partner, claim, "APPROVE_PAYMENT"))
                .isInstanceOf(RuntimeException.class)
                .hasMessageContaining("not allowed");
    }
}

OpenShift Deployment Smoke Test als Checkliste

### OpenShiftDeploymentSmokeTest

### Vorbedingungen

- Namespace existiert.
- Image ist gebaut und gepusht.
- ConfigMap existiert.
- Secret existiert.
- Route ist erreichbar.
- Oracle-Verbindung ist freigeschaltet.
- MQ-Verbindung ist freigeschaltet.
- LDAP/Identity Provider ist erreichbar.

### PrĂŒfungen

1. oc get pods -n claims-prod
2. Alle Pods Running und Ready.
3. oc logs deployment/claim-service -n claims-prod zeigt keine Startfehler.
4. /claims/health/live liefert OK.
5. /claims/health/ready liefert OK.
6. /claims/openapi erreichbar.
7. SOAP WSDL erreichbar.
8. Testentscheidung fĂŒr Test-Claim lĂ€uft durch.
9. Outbox-Event wird geschrieben.
10. Outbox-CronJob publiziert Event.
11. Audit-Event wird gespeichert.
12. Search Read Model wird aktualisiert.
13. Keine sensiblen Kundendaten im Log.
14. Rollback auf vorherige Deployment-Revision möglich.

Cutover Plan

Phasen

PhaseZielAktion
C0VorbereitungTests, Images, Manifests, Secrets vorbereiten
C1Shadow DeploymentOpenShift-Service ohne produktiven Traffic deployen
C2Read-only ChecksHealth, DB, SOAP, REST, Rollen prĂŒfen
C3SOAP ParalleltestSOAP Requests gegen Legacy und Neu vergleichen
C4Partner PilotausgewÀhlte Partner auf neue Route leiten
C5Backoffice PilotausgewÀhlte interne Nutzer umstellen
C6Event CutoverOutbox/MQ produktiv aktivieren
C7Batch CutoverEJB Timer deaktivieren, CronJobs aktivieren
C8VollbetriebTraffic vollstÀndig auf OpenShift
C9Legacy FreezeWebSphere nur noch Fallback/Read-only

Rollback-Plan

Rollback ist möglich, solange:

Rollback Plan

#### ROLLBACK_PLAN

### Sofort-Rollback

1. OpenShift Route zurĂŒck auf Legacy Gateway zeigen lassen.
2. Neue CronJobs suspendieren:
   oc patch cronjob claim-sla-escalation -p '{"spec":{"suspend":true}}'
3. Outbox Publisher pausieren.
4. WebSphere TimerBeans wieder aktivieren, falls sie deaktiviert wurden.
5. Monitoring auf doppelte Events prĂŒfen.
6. Datenbankstatus mit Golden-Master-Vergleich prĂŒfen.

### Nicht erlaubt

- OpenShift Batch und WebSphere Timer gleichzeitig aktiv lassen.
- Payment Release ohne Idempotency aktivieren.
- Partner Portal ohne SOAP-KompatibilitÀt umleiten.

Migration Decisions

#### MIGRATION_DECISIONS.md

### MD-001: Keine Big-Bang-Migration

Entscheidung:
Die Migration erfolgt schrittweise mit Strangler-Ansatz.

Warum:
Das Legacy-System hat viele externe AbhÀngigkeiten und Fachrisiken.

Nutzen:
Geringeres Ausfallrisiko, bessere Testbarkeit, parallele Vergleichbarkeit.

Risiko:
Übergangsphase ist lĂ€nger und braucht saubere Schnittstellen.

### MD-002: SOAP bleibt zunÀchst kompatibel

Entscheidung:
SOAP-Endpunkte werden in der Zielruntime weiterhin angeboten.

Warum:
Partner, Backoffice und externe Systeme können nicht gleichzeitig migriert werden.

Nutzen:
Technische Migration wird von Client-Migration entkoppelt.

Risiko:
SOAP-KompatibilitÀt erzeugt Zusatzaufwand.

### MD-003: EJB Timer wird durch OpenShift CronJob ersetzt

Entscheidung:
SLA/Eskalationslogik wird in claim-batch verschoben.

Warum:
EJB Timer ist WebSphere-gebunden und schwer zu skalieren.

Nutzen:
Bessere Betriebssteuerung, klare Job-Historie, bessere Testbarkeit.

Risiko:
Doppelte AusfĂŒhrung muss durch Lease Lock verhindert werden.

### MD-004: Outbox statt direkter JMS-Publizierung

Entscheidung:
Fachliche Events werden zuerst in CLAIM_OUTBOX persistiert.

Warum:
DB-Änderung und Event-Publikation mĂŒssen konsistent sein.

Nutzen:
Retry, Idempotency, Auditierbarkeit.

Risiko:
Outbox Publisher muss ĂŒberwacht werden.

### MD-005: FileShare nur als Übergangsadapter

Entscheidung:
FileShareDocumentAdapter bleibt fĂŒr Migration erhalten.

Warum:
Dokumentenbestand kann nicht sofort in Object Storage migriert werden.

Nutzen:
Schrittweise Ablösung möglich.

Risiko:
RWX Storage und Netzwerkzugriff mĂŒssen stabil betrieben werden.

Risiken

#### RISKS.md

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

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

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

### R-002: Doppelte Zahlungen

Auswirkung:
Finanzieller Schaden.

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

### R-003: Doppelte Eskalationen

Auswirkung:
Falsche Benachrichtigungen, Prozesschaos.

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

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

Auswirkung:
Dokumente nicht erreichbar.

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

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

Auswirkung:
Unberechtigte Bearbeitung oder blockierte Nutzer.

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

### R-006: Stored Procedure Seiteneffekte vergessen

Auswirkung:
Fachliche Abweichungen.

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

Before/After-Migrationsmapping Runde 5

LegacyOpenShift-ZielGrundSicherheitsnetz
WebSphere EARclaim-service DeploymentRuntime entkoppelnDeployment Smoke Test
EJB TimerOpenShift CronJobScheduler portabel machenBatch Lease Lock Test
direkte JMS-PublizierungOutbox + Publisher CronJobTransaktionssicherheitOutboxPublisherIT
JNDI DataSourceConfig/Secret/DataSource Configreproduzierbare KonfigurationOracleConnectivityIT
JAAS SessionContextSecurityContextPortSecurity abstrahierenRoleMappingIT
FileShare HardcodingDocumentStoragePort + PVC/Object StorageDokumentenablage austauschbarDocumentAdapterIT
WebSphere Console ConfigYAML/ConfigMap/SecretGitOps-fÀhigStructure Check
technische LogsJSON Logs + Audit EventsObservabilityLog Sampling Check
manuelles DeploymentOpenShift Manifests/PipelineWiederholbarkeitPipeline Smoke Test

Changelog-V2-Vorbereitung fĂŒr Runde 5

Neue Changelog-Kandidaten:

IDKategorieÄnderung
CHG-OS-001RuntimeWebSphere EAR in OpenShift Deployment ĂŒberfĂŒhrt
CHG-OS-002ContainerOpen Liberty Containerfile ergÀnzt
CHG-OS-003ContainerSpring Boot Alternative ergÀnzt
CHG-OS-004ConfigWebSphere Console Config durch ConfigMap/Secret ersetzt
CHG-OS-005HealthLiveness/Readiness/Startup Probes eingefĂŒhrt
CHG-OS-006BatchEJB Timer durch OpenShift CronJob ersetzt
CHG-OS-007Messagingdirekte JMS-Publizierung durch Outbox Publisher ersetzt
CHG-OS-008SecurityJAAS-Rollenmapping in AuthorizationPolicy ĂŒberfĂŒhrt
CHG-OS-009StorageFileShare-Strategie containerfÀhig gemacht
CHG-OS-010ObservabilityLogging/Metrics/Tracing ergÀnzt
CHG-OS-011NetworkNetworkPolicy fĂŒr externe Systeme vorbereitet
CHG-OS-012TestsOpenShift Migration Tests ergÀnzt
CHG-OS-013RollbackCutover- und Rollback-Plan ergÀnzt

Zusammenfassung Runde 5

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

7. Lernbuch-Artefakte und Ausgabeformate

Kapitelziel

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

Ziel dieser Runde

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

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

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

<details>
  <summary>Analyse anzeigen</summary>
  <p>Die Fachlogik ist direkt mit EJB, SOAP, JDBC, JMS und File Shares gekoppelt.</p>
</details>

Beispiel: echte Navigation statt JavaScript-Button

<nav>
  <a href="#zielbild">Zielbild</a>
  <a href="#legacy">Legacy-System</a>
  <a href="#refactoring">Refactoring</a>
</nav>

Beispiel: Codeblock fĂŒr PDF/HTML kompakt halten

package com.seb4u.demo.claims.application;

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

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Claim claim = claimRepository.getRequired(command.claimId());
        transitionPolicy.assertAllowed(claim.status(), command.decisionType());
        documentVerificationPort.verifyRequiredDocuments(command.claimId());
        fraudRiskPort.assessRisk(command.claimId());
        coveragePort.checkCoverage(command.claimId());
        paymentReleasePort.preparePaymentIfRequired(command.claimId(), command.decisionType());
        auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
        return ProcessClaimDecisionResult.accepted(command.claimId());
    }
}

8. Änderungsdokumentation und Mapping

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Änderungsdokumentation und Mapping in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Erzeugt am: 2026-07-05T11:05:00+02:00

Diese Runde ergĂ€nzt die bisherige Lernprojekt-Arbeit um die Änderungsdokumentation. Sie macht sichtbar, welche Legacy-Dateien, technischen Schulden und Kopplungen in welche modernen Module, Ports, Policies, Handler und Adapter ĂŒberfĂŒhrt werden.

Ergebnis dieser Runde

Refactoring-Phasen

PhaseTitelZielTechnik/ÄnderungSicherheitsnetzRisiko
01Inventory und CharacterizationSysteme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisierenKeine Änderung am ProduktionscodeInventory-Dokument, Golden-Master-SnapshotFalsche Systemannahmen
02Systemlandschaft und Zusatzsysteme erfassenPartner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreibenKontextgrenzen sichtbarSystem Landscape Mapverdeckte Kopplungen
03Monster Method verstehenprocessClaimDecision(...) absichern und fachliche Schritte markierenSeams, Logging, TestdatenCharacterization TestTests bilden Bugs mit ab
04Facade und Command extrahierenInput in Command Object und Result Object ĂŒberfĂŒhrenCommand HandlerHandler TestTransaktionskontext prĂŒfen
05Statuslogik in State/Policy ĂŒberfĂŒhrenif/else durch ClaimTransitionPolicy ersetzenState Pattern, Policy ObjectState Transition TestSonderstatus fehlen
06Coverage/Fraud/Document/Payment Ports extrahierendirekte SOAP/File/Payment Calls ablösenPorts & AdaptersAdapter Contract TestsFehlersemantik angleichen
07Customer und Identity Ports extrahierenStammdaten und RollenprĂŒfung isolierenACL, AuthorizationPolicyRole Mapping TestRollenmigration
08Workflow Orchestrator einfĂŒhrenAblauf koordinieren, Fachlogik in Use Cases haltenWorkflow OrchestratorWorkflow Scenario TestGod-Orchestrator vermeiden
09Audit Trail abstrahierenfachliche Audit Events statt technischer LogsAuditLogPortAudit Snapshot TestPII/Compliance
10Search, Notification, Reporting trennenseiteneffektarme Boundaries schaffenRead Model, OutboxSearch/Notification TestsEventual consistency
11Outbox und Idempotency einfĂŒhrenJMS/Payment sicher wiederholbar machenOutbox Pattern, Idempotency KeyReplay/Idempotency TestsDoppelte externe Effekte
12Batch und SLA trennenTimerBeans in Batch Worker und Policy ĂŒberfĂŒhrenBatch Worker, Lease LockBatch Lease TestDoppelte Jobs
13Portal und Backoffice entkoppelnUI ĂŒber REST/BFF schrittweise strangelnStrangler FigContract Testsparallele UI-Pfade
14OpenShift-Migration vorbereitenRuntime Boundary, Config, Health, Security, ObservabilityContainer, ConfigMap, Secret, CronJobDeployment Smoke TestRuntime-Unterschiede

Essential Changes

IDÄnderungVorherNachherPattern / KonzeptRelevante OrdnerTest / SicherheitsnetzMigrationsnutzen
CLG-001Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemachtNur Zielarchitektur diskutiert/before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbarArchitecture Baselinedocs, legacy-websphere-earCharacterization TestsMigration startet mit belegbarem Ist-Zustand
CLG-002Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogenEJB enthÀlt Fach-, Transaktions-, SOAP-, JMS- und DAO-Logikclaim-domain und claim-application enthalten GeschÀftsregelnHexagonal Architectureclaim-domain, claim-applicationUnit Tests, ArchUnitFachlogik ohne WebSphere testbar
CLG-003processClaimDecision(...) in fachliche Schritte zerlegtEine große Methode entscheidet, prĂŒft, speichert, publiziertCommand Handler + Workflow Orchestrator + PortsCommand Handler, Orchestratorclaim-application, claim-workflowGolden Master, Handler TestsSchrittweise Migration möglich
CLG-004Claim Statuslogik aus if/else-Ketten herausgezogenStatusĂŒbergĂ€nge in verschachtelten if/elseClaimState + ClaimTransitionPolicyState Pattern, Policy Objectclaim-domainState Transition TestsRegeln werden sichtbar und Ă€nderbar
CLG-005Workflow Orchestrator eingefĂŒhrtWorkflow versteckt im EJBClaimDecisionWorkflowOrchestrator koordiniert SchritteWorkflow Orchestratorclaim-workflowWorkflow Scenario TestsAblauf kann spĂ€ter als Service laufen
CLG-006Ports & Adapters eingefĂŒhrtDirekte SOAP/JMS/JDBC/File-AufrufeFachlogik ruft Ports auf, Infrastruktur implementiert AdapterPorts & Adaptersclaim-application, claim-infrastructureAdapter Contract TestsTechnik austauschbar
CLG-007SOAP Boundary kompatibel gehaltenPartner hĂ€ngen direkt an Legacy SOAP Endpointclaim-soap-api ĂŒbersetzt SOAP auf Use CasesFacade, Anti-Corruption Layerclaim-soap-apiSOAP Compatibility TestsPartner mĂŒssen nicht sofort migrieren
CLG-008REST/OpenAPI ergĂ€nztKeine moderne API fĂŒr neue UIsclaim-rest-api + claim-openapiAPI Facadeclaim-rest-api, claim-openapiREST Smoke TestsBFF/Frontend-Strangler möglich
CLG-009Document Adapter eingefĂŒhrtFile Shares hart verdrahtetDocumentStoragePort + FileShareDocumentAdapterAdapter, ACLclaim-documentAdapter TestsspĂ€ter Object Storage möglich
CLG-010Fraud Risk Gateway entkoppeltDirekter SOAP Client im FachflussFraudRiskPort + FraudRiskSoapAdapterAnti-Corruption Layerclaim-infrastructureContract Test, Timeout TestFehler-/Retry-Politik steuerbar
CLG-011Policy Coverage ĂŒber CoveragePort abstrahiertCoverage SOAP/Stored Procedure direkt genutztCoveragePort + CoverageDecisionSpecification, Policy Objectclaim-domain, claim-infrastructureCoverage TestsAlte Tariflogik isoliert
CLG-012Payment Release entkoppeltZahlung direkt in JTA-Fluss ausgelöstPaymentReleasePort + Outbox/CompensationOutbox, Idempotencyclaim-paymentIdempotency TestsZahlungen sicherer migrierbar
CLG-013NotificationPort modernisiertEJB sendet JMS/SOAP/E-Mail direktNotificationPort + NotificationAdapterAdapter, Outboxclaim-notificationDLQ/Retry TestsBenachrichtigung entkoppelt
CLG-014SLA & Escalation Scheduler getrenntEJB Timer mit DB-SeiteneffektenEscalationBatchJob + EscalationPolicyBatch Worker, Policy Objectclaim-batchBatch Lease TestsCronJob auf OpenShift möglich
CLG-015Audit & Compliance Boundary getrenntTechnische Logs und Stored Procedures vermischtAuditLogPort + Reporting Read ModelAudit Event, Read Modelclaim-auditAudit Snapshot TestsCompliance nachvollziehbar
CLG-016Search Indexer ĂŒber ClaimSearchPort abstrahiertOracle LIKE Queries in DAO/BackofficeClaimSearchPort + SearchIndexAdapterRead Model, Adapterclaim-searchSearch Adapter TestsspĂ€ter OpenSearch möglich
CLG-017Customer Master Data entkoppeltDirekter SOAP Client und alte KundennummernCustomerMasterDataPort + CustomerSnapshotACL, Value Objectclaim-infrastructureCustomer Contract TestsDubletten/Inkonsistenz isoliert
CLG-018Identity & Role Management abstrahiertJAAS/WebSphere-Rollen hart kodiertSecurityContextPort + AuthorizationPolicyPolicy Object, Role Mappingclaim-application, claim-infrastructureRole Mapping TestsIdP-Migration vorbereitet
CLG-019Outbox und Idempotency eingefĂŒhrtJMS/Payment im gleichen Fluss ohne WiederholschutzOutboxEvent + IdempotencyKeyOutbox Patternclaim-infrastructure, claim-paymentOutbox Publisher Testsrobuster bei Pod-Restarts
CLG-020OpenShift-/Runtime-Migration vorbereitetEAR manuell auf WebSphere deploytContainer, Deployment, Route, ConfigMap, SecretRuntime Boundarymigration_to_openshiftDeployment Smoke TestsAutomatisierbare Migration

Changelog V2

IDKategorieÄnderungVorherNachherPattern / KonzeptModuleTestsMigrationsnutzenRisiko
CLG-001Legacy sichtbar gemachtLegacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemachtNur Zielarchitektur diskutiert/before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbarArchitecture Baselinedocs, legacy-websphere-earCharacterization TestsMigration startet mit belegbarem Ist-ZustandAlte Annahmen können falsch sein
CLG-002FachlogikFachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogenEJB enthÀlt Fach-, Transaktions-, SOAP-, JMS- und DAO-Logikclaim-domain und claim-application enthalten GeschÀftsregelnHexagonal Architectureclaim-domain, claim-applicationUnit Tests, ArchUnitFachlogik ohne WebSphere testbarTransaktionsgrenzen neu bewerten
CLG-003Monster MethodprocessClaimDecision(...) in fachliche Schritte zerlegtEine große Methode entscheidet, prĂŒft, speichert, publiziertCommand Handler + Workflow Orchestrator + PortsCommand Handler, Orchestratorclaim-application, claim-workflowGolden Master, Handler TestsSchrittweise Migration möglichVerhalten muss identisch bleiben
CLG-004StatuslogikClaim Statuslogik aus if/else-Ketten herausgezogenStatusĂŒbergĂ€nge in verschachtelten if/elseClaimState + ClaimTransitionPolicyState Pattern, Policy Objectclaim-domainState Transition TestsRegeln werden sichtbar und Ă€nderbarSonderfĂ€lle ĂŒbersehen
CLG-005WorkflowWorkflow Orchestrator eingefĂŒhrtWorkflow versteckt im EJBClaimDecisionWorkflowOrchestrator koordiniert SchritteWorkflow Orchestratorclaim-workflowWorkflow Scenario TestsAblauf kann spĂ€ter als Service laufenZu viel Logik im Orchestrator vermeiden
CLG-006PortsPorts & Adapters eingefĂŒhrtDirekte SOAP/JMS/JDBC/File-AufrufeFachlogik ruft Ports auf, Infrastruktur implementiert AdapterPorts & Adaptersclaim-application, claim-infrastructureAdapter Contract TestsTechnik austauschbarPort-Schnittstellen nicht zu technisch machen
CLG-007SOAPSOAP Boundary kompatibel gehaltenPartner hĂ€ngen direkt an Legacy SOAP Endpointclaim-soap-api ĂŒbersetzt SOAP auf Use CasesFacade, Anti-Corruption Layerclaim-soap-apiSOAP Compatibility TestsPartner mĂŒssen nicht sofort migrierenSchema-KompatibilitĂ€t prĂŒfen
CLG-008RESTREST/OpenAPI ergĂ€nztKeine moderne API fĂŒr neue UIsclaim-rest-api + claim-openapiAPI Facadeclaim-rest-api, claim-openapiREST Smoke TestsBFF/Frontend-Strangler möglichDoppelte API-Semantik vermeiden
CLG-009DocumentDocument Adapter eingefĂŒhrtFile Shares hart verdrahtetDocumentStoragePort + FileShareDocumentAdapterAdapter, ACLclaim-documentAdapter TestsspĂ€ter Object Storage möglichDateipfade/Permissions
CLG-010FraudFraud Risk Gateway entkoppeltDirekter SOAP Client im FachflussFraudRiskPort + FraudRiskSoapAdapterAnti-Corruption Layerclaim-infrastructureContract Test, Timeout TestFehler-/Retry-Politik steuerbarFachliche vs technische Fehler trennen
CLG-011CoveragePolicy Coverage ĂŒber CoveragePort abstrahiertCoverage SOAP/Stored Procedure direkt genutztCoveragePort + CoverageDecisionSpecification, Policy Objectclaim-domain, claim-infrastructureCoverage TestsAlte Tariflogik isoliertRegelabweichungen dokumentieren
CLG-012PaymentPayment Release entkoppeltZahlung direkt in JTA-Fluss ausgelöstPaymentReleasePort + Outbox/CompensationOutbox, Idempotencyclaim-paymentIdempotency TestsZahlungen sicherer migrierbarDoppelauszahlung vermeiden
CLG-013NotificationNotificationPort modernisiertEJB sendet JMS/SOAP/E-Mail direktNotificationPort + NotificationAdapterAdapter, Outboxclaim-notificationDLQ/Retry TestsBenachrichtigung entkoppeltTemplates versionieren
CLG-014SLASLA & Escalation Scheduler getrenntEJB Timer mit DB-SeiteneffektenEscalationBatchJob + EscalationPolicyBatch Worker, Policy Objectclaim-batchBatch Lease TestsCronJob auf OpenShift möglichDoppelverarbeitung verhindern
CLG-015AuditAudit & Compliance Boundary getrenntTechnische Logs und Stored Procedures vermischtAuditLogPort + Reporting Read ModelAudit Event, Read Modelclaim-auditAudit Snapshot TestsCompliance nachvollziehbarDatenschutz/PII beachten
CLG-016SearchSearch Indexer ĂŒber ClaimSearchPort abstrahiertOracle LIKE Queries in DAO/BackofficeClaimSearchPort + SearchIndexAdapterRead Model, Adapterclaim-searchSearch Adapter TestsspĂ€ter OpenSearch möglichIndexkonsistenz
CLG-017CustomerCustomer Master Data entkoppeltDirekter SOAP Client und alte KundennummernCustomerMasterDataPort + CustomerSnapshotACL, Value Objectclaim-infrastructureCustomer Contract TestsDubletten/Inkonsistenz isoliertDatenqualitÀt bleibt Thema
CLG-018IdentityIdentity & Role Management abstrahiertJAAS/WebSphere-Rollen hart kodiertSecurityContextPort + AuthorizationPolicyPolicy Object, Role Mappingclaim-application, claim-infrastructureRole Mapping TestsIdP-Migration vorbereitetRollenmatrix abgleichen
CLG-019OutboxOutbox und Idempotency eingefĂŒhrtJMS/Payment im gleichen Fluss ohne WiederholschutzOutboxEvent + IdempotencyKeyOutbox Patternclaim-infrastructure, claim-paymentOutbox Publisher Testsrobuster bei Pod-RestartsEventual Consistency erklĂ€ren
CLG-020OpenShiftOpenShift-/Runtime-Migration vorbereitetEAR manuell auf WebSphere deploytContainer, Deployment, Route, ConfigMap, SecretRuntime Boundarymigration_to_openshiftDeployment Smoke TestsAutomatisierbare MigrationRuntime-Unterschiede prĂŒfen

Before/After-Mapping

Legacy-Datei / ModulNeue Datei / neues ModulGrundPattern / ArchitekturkonzeptTest / SicherheitsnetzMigrationsnutzen
LegacyClaimFacadeBean.javaProcessClaimDecisionHandler.javaMonster Method zerlegtCommand Handler, Application ServiceGolden Master + Handler TestFachlogik kann unabhÀngig von WebSphere getestet und migriert werden
LegacyClaimStatusIfElse.javaClaimState.java + ClaimTransitionPolicy.javaStatuslogik aus if/else-Ketten herausgezogenState Pattern, Policy ObjectState Transition TestWorkflow-Regeln werden verstÀndlich und testbar
LegacyDocumentShareClient.javaDocumentStoragePort.java + FileShareDocumentAdapter.javaFile Share entkoppeltPort, Adapter, Anti-Corruption LayerAdapter TestDokumentenablage kann spÀter ersetzt werden
LegacyPartnerPortalServlet.javaPartnerPortalRestController.java / PartnerPortalBffAdapter.javaPartner Portal vom EJB getrenntStrangler UI, BFF AdapterPortal Contract TestExternes Portal kann schrittweise modernisiert werden
InternalClaimsBackofficeBean.javaClaimBackofficeUseCase.javaBackoffice-Logik aus JSP/EJB herausgezogenUse Case, Application ServiceBackoffice Use Case TestInterne UI kann spÀter Angular/React nutzen
FraudRiskSoapClient.javaFraudRiskPort.java + FraudRiskSoapAdapter.javaFraud SOAP entkoppeltPort, Adapter, ACLFraud Contract TestTimeout/Retry/Circuit Breaker möglich
PolicyCoverageSoapClient.javaCoveragePort.java + CoverageSoapAdapter.javaCoverage-System isoliertSpecification, Policy Object, AdapterCoverage Decision TestAlte Tariflogik kontrolliert migrierbar
PaymentReleaseSoapClient.javaPaymentReleasePort.java + PaymentReleaseSoapAdapter.javaPayment-Freigabe entkoppeltPort, Adapter, IdempotencyPayment Idempotency TestZahlungsrisiken werden reduziert
LegacyNotificationSenderBean.javaNotificationPort.java + NotificationAdapter.javaBenachrichtigung entkoppeltOutbox, AdapterNotification Retry TestDLQ/Retry auf Plattformebene möglich
SlaEscalationTimerBean.javaEscalationBatchJob.java + EscalationPolicy.javaEJB Timer ersetztBatch Worker, Policy Object, Lease LockBatch Lease TestOpenShift CronJob möglich
AuditReportStoredProcedureDao.javaAuditLogPort.java + AuditReportingAdapter.javaAudit/Reporting getrenntAudit Event, Reporting Read ModelAudit Trail TestCompliance-Berichte werden fachlich nachvollziehbar
LegacyClaimSearchDao.javaClaimSearchPort.java + SearchIndexAdapter.javaDB-LIKE-Suche entkoppeltRead Model, AdapterSearch Adapter TestspÀter Elasticsearch/OpenSearch möglich
CustomerMasterDataSoapClient.javaCustomerMasterDataPort.java + CustomerMasterDataSoapAdapter.javaCustomer-Stammdaten entkoppeltACL, Snapshot Value ObjectCustomer Contract TestDatenqualitÀtsprobleme werden isoliert
JaasRoleChecker.javaSecurityContextPort.java + AuthorizationPolicy.javaRollenprĂŒfung zentralisiertPolicy Object, Role MappingSecurity Role Mapping TestOpenShift/IdP-Migration vorbereitet
LegacyClaimEventPublisher.javaOutboxEvent.java + OutboxPublisherWorker.javaDirekte JMS-Publizierung abgelöstOutbox PatternOutbox Replay Testrobust gegen Transaktions- und Pod-Fehler
LegacyClaimDao.javaClaimRepository.java + JpaClaimRepository.javaDAO und Fachmodell getrenntRepository, Data MapperRepository Integration TestDB-Zugriff ist austauschbar
ClaimDecisionSoapEndpoint.javaClaimDecisionSoapEndpoint.java + ProcessClaimDecisionHandler.javaSOAP bleibt, delegiert aber nurFacade, ACLSOAP Compatibility ITPartner bleiben kompatibel
LegacyAuditLogger.javaAuditLogPort.java + StructuredAuditLogAdapter.javaversteckte Auditlogik sichtbar gemachtPort, Structured LoggingAudit Snapshot TestAudit kann zentral ausgewertet werden

Codebeispiele

Zentrale Codebeispiele

State/Policy statt if/else

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

public final class ClaimTransitionPolicy {
    public void assertAllowed(ClaimStatus current, ClaimDecisionType decision) {
        if (current == ClaimStatus.CLOSED) {
            throw new ClaimTransitionNotAllowedException("Closed claims cannot be changed");
        }
        if (decision == ClaimDecisionType.APPROVE_PAYMENT && current != ClaimStatus.DOCUMENTS_VERIFIED) {
            throw new ClaimTransitionNotAllowedException("Payment approval requires verified documents");
        }
    }
}

Command Handler als neue Use-Case-Grenze

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

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

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Claim claim = repository.getRequired(command.claimId());
        transitionPolicy.assertAllowed(claim.status(), command.decisionType());
        var documentDecision = documentVerificationPort.verify(command.claimId(), command.documentIds());
        var risk = fraudRiskPort.calculateRisk(command.claimId(), claim.customerId());
        var coverage = coveragePort.checkCoverage(claim.policyId(), command.claimId());
        claim.applyDecision(command.decisionType(), documentDecision, risk, coverage);
        repository.save(claim);
        auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
        return ProcessClaimDecisionResult.from(claim);
    }
}

Mapping-Test gegen Legacy-Verhalten

package com.seb4u.demo.claims.migrationtests;

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

9. Anhang A - Refactoring Phases

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang A - Refactoring Phases in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

REFACTORING_PHASES

Refactoring-Phasen-Paket fĂŒr das Legacy Claims & Customer Support Enterprise System.

PhaseTitelZielTechnik/ÄnderungSicherheitsnetzRisiko
01Inventory und CharacterizationSysteme, EAR/WAR/EJB/SOAP/JMS/Oracle/LDAP/FileShare inventarisierenKeine Änderung am ProduktionscodeInventory-Dokument, Golden-Master-SnapshotFalsche Systemannahmen
02Systemlandschaft und Zusatzsysteme erfassenPartner Portal, Backoffice, Document, Fraud, Coverage, Payment, Notification, SLA, Audit, Search, Customer, Identity beschreibenKontextgrenzen sichtbarSystem Landscape Mapverdeckte Kopplungen
03Monster Method verstehenprocessClaimDecision(...) absichern und fachliche Schritte markierenSeams, Logging, TestdatenCharacterization TestTests bilden Bugs mit ab
04Facade und Command extrahierenInput in Command Object und Result Object ĂŒberfĂŒhrenCommand HandlerHandler TestTransaktionskontext prĂŒfen
05Statuslogik in State/Policy ĂŒberfĂŒhrenif/else durch ClaimTransitionPolicy ersetzenState Pattern, Policy ObjectState Transition TestSonderstatus fehlen
06Coverage/Fraud/Document/Payment Ports extrahierendirekte SOAP/File/Payment Calls ablösenPorts & AdaptersAdapter Contract TestsFehlersemantik angleichen
07Customer und Identity Ports extrahierenStammdaten und RollenprĂŒfung isolierenACL, AuthorizationPolicyRole Mapping TestRollenmigration
08Workflow Orchestrator einfĂŒhrenAblauf koordinieren, Fachlogik in Use Cases haltenWorkflow OrchestratorWorkflow Scenario TestGod-Orchestrator vermeiden
09Audit Trail abstrahierenfachliche Audit Events statt technischer LogsAuditLogPortAudit Snapshot TestPII/Compliance
10Search, Notification, Reporting trennenseiteneffektarme Boundaries schaffenRead Model, OutboxSearch/Notification TestsEventual consistency
11Outbox und Idempotency einfĂŒhrenJMS/Payment sicher wiederholbar machenOutbox Pattern, Idempotency KeyReplay/Idempotency TestsDoppelte externe Effekte
12Batch und SLA trennenTimerBeans in Batch Worker und Policy ĂŒberfĂŒhrenBatch Worker, Lease LockBatch Lease TestDoppelte Jobs
13Portal und Backoffice entkoppelnUI ĂŒber REST/BFF schrittweise strangelnStrangler FigContract Testsparallele UI-Pfade
14OpenShift-Migration vorbereitenRuntime Boundary, Config, Health, Security, ObservabilityContainer, ConfigMap, Secret, CronJobDeployment Smoke TestRuntime-Unterschiede

Phasenordner

01_before_refactoring_legacy/
02_refactoring_phases/
03_after_refactoring_modular/
04_migration_to_openshift/
05_docs_and_tests/
06_original_project_zip/

Phase 01 – Inventory und Characterization

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

Änderung: Keine Änderung am Produktionscode

Sicherheitsnetz: Inventory-Dokument, Golden-Master-Snapshot

Risiko: Falsche Systemannahmen

Phase 02 – Systemlandschaft und Zusatzsysteme erfassen

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

Änderung: Kontextgrenzen sichtbar

Sicherheitsnetz: System Landscape Map

Risiko: verdeckte Kopplungen

Phase 03 – Monster Method verstehen

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

Änderung: Seams, Logging, Testdaten

Sicherheitsnetz: Characterization Test

Risiko: Tests bilden Bugs mit ab

Phase 04 – Facade und Command extrahieren

Ziel: Input in Command Object und Result Object ĂŒberfĂŒhren

Änderung: Command Handler

Sicherheitsnetz: Handler Test

Risiko: Transaktionskontext prĂŒfen

Phase 05 – Statuslogik in State/Policy ĂŒberfĂŒhren

Ziel: if/else durch ClaimTransitionPolicy ersetzen

Änderung: State Pattern, Policy Object

Sicherheitsnetz: State Transition Test

Risiko: Sonderstatus fehlen

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

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

Änderung: Ports & Adapters

Sicherheitsnetz: Adapter Contract Tests

Risiko: Fehlersemantik angleichen

Phase 07 – Customer und Identity Ports extrahieren

Ziel: Stammdaten und RollenprĂŒfung isolieren

Änderung: ACL, AuthorizationPolicy

Sicherheitsnetz: Role Mapping Test

Risiko: Rollenmigration

Phase 08 – Workflow Orchestrator einfĂŒhren

Ziel: Ablauf koordinieren, Fachlogik in Use Cases halten

Änderung: Workflow Orchestrator

Sicherheitsnetz: Workflow Scenario Test

Risiko: God-Orchestrator vermeiden

Phase 09 – Audit Trail abstrahieren

Ziel: fachliche Audit Events statt technischer Logs

Änderung: AuditLogPort

Sicherheitsnetz: Audit Snapshot Test

Risiko: PII/Compliance

Phase 10 – Search, Notification, Reporting trennen

Ziel: seiteneffektarme Boundaries schaffen

Änderung: Read Model, Outbox

Sicherheitsnetz: Search/Notification Tests

Risiko: Eventual consistency

Phase 11 – Outbox und Idempotency einfĂŒhren

Ziel: JMS/Payment sicher wiederholbar machen

Änderung: Outbox Pattern, Idempotency Key

Sicherheitsnetz: Replay/Idempotency Tests

Risiko: Doppelte externe Effekte

Phase 12 – Batch und SLA trennen

Ziel: TimerBeans in Batch Worker und Policy ĂŒberfĂŒhren

Änderung: Batch Worker, Lease Lock

Sicherheitsnetz: Batch Lease Test

Risiko: Doppelte Jobs

Phase 13 – Portal und Backoffice entkoppeln

Ziel: UI ĂŒber REST/BFF schrittweise strangeln

Änderung: Strangler Fig

Sicherheitsnetz: Contract Tests

Risiko: parallele UI-Pfade

Phase 14 – OpenShift-Migration vorbereiten

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

Änderung: Container, ConfigMap, Secret, CronJob

Sicherheitsnetz: Deployment Smoke Test

Risiko: Runtime-Unterschiede

10. Anhang B - Essential Changes

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang B - Essential Changes in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

ESSENTIAL CHANGES – Change Catalog

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

IDÄnderungVorherNachherPattern / KonzeptRelevante OrdnerTest / SicherheitsnetzMigrationsnutzen
CLG-001Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemachtNur Zielarchitektur diskutiert/before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbarArchitecture Baselinedocs, legacy-websphere-earCharacterization TestsMigration startet mit belegbarem Ist-Zustand
CLG-002Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogenEJB enthÀlt Fach-, Transaktions-, SOAP-, JMS- und DAO-Logikclaim-domain und claim-application enthalten GeschÀftsregelnHexagonal Architectureclaim-domain, claim-applicationUnit Tests, ArchUnitFachlogik ohne WebSphere testbar
CLG-003processClaimDecision(...) in fachliche Schritte zerlegtEine große Methode entscheidet, prĂŒft, speichert, publiziertCommand Handler + Workflow Orchestrator + PortsCommand Handler, Orchestratorclaim-application, claim-workflowGolden Master, Handler TestsSchrittweise Migration möglich
CLG-004Claim Statuslogik aus if/else-Ketten herausgezogenStatusĂŒbergĂ€nge in verschachtelten if/elseClaimState + ClaimTransitionPolicyState Pattern, Policy Objectclaim-domainState Transition TestsRegeln werden sichtbar und Ă€nderbar
CLG-005Workflow Orchestrator eingefĂŒhrtWorkflow versteckt im EJBClaimDecisionWorkflowOrchestrator koordiniert SchritteWorkflow Orchestratorclaim-workflowWorkflow Scenario TestsAblauf kann spĂ€ter als Service laufen
CLG-006Ports & Adapters eingefĂŒhrtDirekte SOAP/JMS/JDBC/File-AufrufeFachlogik ruft Ports auf, Infrastruktur implementiert AdapterPorts & Adaptersclaim-application, claim-infrastructureAdapter Contract TestsTechnik austauschbar
CLG-007SOAP Boundary kompatibel gehaltenPartner hĂ€ngen direkt an Legacy SOAP Endpointclaim-soap-api ĂŒbersetzt SOAP auf Use CasesFacade, Anti-Corruption Layerclaim-soap-apiSOAP Compatibility TestsPartner mĂŒssen nicht sofort migrieren
CLG-008REST/OpenAPI ergĂ€nztKeine moderne API fĂŒr neue UIsclaim-rest-api + claim-openapiAPI Facadeclaim-rest-api, claim-openapiREST Smoke TestsBFF/Frontend-Strangler möglich
CLG-009Document Adapter eingefĂŒhrtFile Shares hart verdrahtetDocumentStoragePort + FileShareDocumentAdapterAdapter, ACLclaim-documentAdapter TestsspĂ€ter Object Storage möglich
CLG-010Fraud Risk Gateway entkoppeltDirekter SOAP Client im FachflussFraudRiskPort + FraudRiskSoapAdapterAnti-Corruption Layerclaim-infrastructureContract Test, Timeout TestFehler-/Retry-Politik steuerbar
CLG-011Policy Coverage ĂŒber CoveragePort abstrahiertCoverage SOAP/Stored Procedure direkt genutztCoveragePort + CoverageDecisionSpecification, Policy Objectclaim-domain, claim-infrastructureCoverage TestsAlte Tariflogik isoliert
CLG-012Payment Release entkoppeltZahlung direkt in JTA-Fluss ausgelöstPaymentReleasePort + Outbox/CompensationOutbox, Idempotencyclaim-paymentIdempotency TestsZahlungen sicherer migrierbar
CLG-013NotificationPort modernisiertEJB sendet JMS/SOAP/E-Mail direktNotificationPort + NotificationAdapterAdapter, Outboxclaim-notificationDLQ/Retry TestsBenachrichtigung entkoppelt
CLG-014SLA & Escalation Scheduler getrenntEJB Timer mit DB-SeiteneffektenEscalationBatchJob + EscalationPolicyBatch Worker, Policy Objectclaim-batchBatch Lease TestsCronJob auf OpenShift möglich
CLG-015Audit & Compliance Boundary getrenntTechnische Logs und Stored Procedures vermischtAuditLogPort + Reporting Read ModelAudit Event, Read Modelclaim-auditAudit Snapshot TestsCompliance nachvollziehbar
CLG-016Search Indexer ĂŒber ClaimSearchPort abstrahiertOracle LIKE Queries in DAO/BackofficeClaimSearchPort + SearchIndexAdapterRead Model, Adapterclaim-searchSearch Adapter TestsspĂ€ter OpenSearch möglich
CLG-017Customer Master Data entkoppeltDirekter SOAP Client und alte KundennummernCustomerMasterDataPort + CustomerSnapshotACL, Value Objectclaim-infrastructureCustomer Contract TestsDubletten/Inkonsistenz isoliert
CLG-018Identity & Role Management abstrahiertJAAS/WebSphere-Rollen hart kodiertSecurityContextPort + AuthorizationPolicyPolicy Object, Role Mappingclaim-application, claim-infrastructureRole Mapping TestsIdP-Migration vorbereitet
CLG-019Outbox und Idempotency eingefĂŒhrtJMS/Payment im gleichen Fluss ohne WiederholschutzOutboxEvent + IdempotencyKeyOutbox Patternclaim-infrastructure, claim-paymentOutbox Publisher Testsrobuster bei Pod-Restarts
CLG-020OpenShift-/Runtime-Migration vorbereitetEAR manuell auf WebSphere deploytContainer, Deployment, Route, ConfigMap, SecretRuntime Boundarymigration_to_openshiftDeployment Smoke TestsAutomatisierbare Migration

11. Anhang C - Changelog V2

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang C - Changelog V2 in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

CHANGELOG V2 – Legacy Claims & Customer Support Enterprise System

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

IDKategorieÄnderungVorherNachherPattern / KonzeptModuleTestsMigrationsnutzenRisiko
CLG-001Legacy sichtbar gemachtLegacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemachtNur Zielarchitektur diskutiert/before_refactoring_legacy mit EAR/WAR/EJB/SOAP/JMS/Oracle sichtbarArchitecture Baselinedocs, legacy-websphere-earCharacterization TestsMigration startet mit belegbarem Ist-ZustandAlte Annahmen können falsch sein
CLG-002FachlogikFachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogenEJB enthÀlt Fach-, Transaktions-, SOAP-, JMS- und DAO-Logikclaim-domain und claim-application enthalten GeschÀftsregelnHexagonal Architectureclaim-domain, claim-applicationUnit Tests, ArchUnitFachlogik ohne WebSphere testbarTransaktionsgrenzen neu bewerten
CLG-003Monster MethodprocessClaimDecision(...) in fachliche Schritte zerlegtEine große Methode entscheidet, prĂŒft, speichert, publiziertCommand Handler + Workflow Orchestrator + PortsCommand Handler, Orchestratorclaim-application, claim-workflowGolden Master, Handler TestsSchrittweise Migration möglichVerhalten muss identisch bleiben
CLG-004StatuslogikClaim Statuslogik aus if/else-Ketten herausgezogenStatusĂŒbergĂ€nge in verschachtelten if/elseClaimState + ClaimTransitionPolicyState Pattern, Policy Objectclaim-domainState Transition TestsRegeln werden sichtbar und Ă€nderbarSonderfĂ€lle ĂŒbersehen
CLG-005WorkflowWorkflow Orchestrator eingefĂŒhrtWorkflow versteckt im EJBClaimDecisionWorkflowOrchestrator koordiniert SchritteWorkflow Orchestratorclaim-workflowWorkflow Scenario TestsAblauf kann spĂ€ter als Service laufenZu viel Logik im Orchestrator vermeiden
CLG-006PortsPorts & Adapters eingefĂŒhrtDirekte SOAP/JMS/JDBC/File-AufrufeFachlogik ruft Ports auf, Infrastruktur implementiert AdapterPorts & Adaptersclaim-application, claim-infrastructureAdapter Contract TestsTechnik austauschbarPort-Schnittstellen nicht zu technisch machen
CLG-007SOAPSOAP Boundary kompatibel gehaltenPartner hĂ€ngen direkt an Legacy SOAP Endpointclaim-soap-api ĂŒbersetzt SOAP auf Use CasesFacade, Anti-Corruption Layerclaim-soap-apiSOAP Compatibility TestsPartner mĂŒssen nicht sofort migrierenSchema-KompatibilitĂ€t prĂŒfen
CLG-008RESTREST/OpenAPI ergĂ€nztKeine moderne API fĂŒr neue UIsclaim-rest-api + claim-openapiAPI Facadeclaim-rest-api, claim-openapiREST Smoke TestsBFF/Frontend-Strangler möglichDoppelte API-Semantik vermeiden
CLG-009DocumentDocument Adapter eingefĂŒhrtFile Shares hart verdrahtetDocumentStoragePort + FileShareDocumentAdapterAdapter, ACLclaim-documentAdapter TestsspĂ€ter Object Storage möglichDateipfade/Permissions
CLG-010FraudFraud Risk Gateway entkoppeltDirekter SOAP Client im FachflussFraudRiskPort + FraudRiskSoapAdapterAnti-Corruption Layerclaim-infrastructureContract Test, Timeout TestFehler-/Retry-Politik steuerbarFachliche vs technische Fehler trennen
CLG-011CoveragePolicy Coverage ĂŒber CoveragePort abstrahiertCoverage SOAP/Stored Procedure direkt genutztCoveragePort + CoverageDecisionSpecification, Policy Objectclaim-domain, claim-infrastructureCoverage TestsAlte Tariflogik isoliertRegelabweichungen dokumentieren
CLG-012PaymentPayment Release entkoppeltZahlung direkt in JTA-Fluss ausgelöstPaymentReleasePort + Outbox/CompensationOutbox, Idempotencyclaim-paymentIdempotency TestsZahlungen sicherer migrierbarDoppelauszahlung vermeiden
CLG-013NotificationNotificationPort modernisiertEJB sendet JMS/SOAP/E-Mail direktNotificationPort + NotificationAdapterAdapter, Outboxclaim-notificationDLQ/Retry TestsBenachrichtigung entkoppeltTemplates versionieren
CLG-014SLASLA & Escalation Scheduler getrenntEJB Timer mit DB-SeiteneffektenEscalationBatchJob + EscalationPolicyBatch Worker, Policy Objectclaim-batchBatch Lease TestsCronJob auf OpenShift möglichDoppelverarbeitung verhindern
CLG-015AuditAudit & Compliance Boundary getrenntTechnische Logs und Stored Procedures vermischtAuditLogPort + Reporting Read ModelAudit Event, Read Modelclaim-auditAudit Snapshot TestsCompliance nachvollziehbarDatenschutz/PII beachten
CLG-016SearchSearch Indexer ĂŒber ClaimSearchPort abstrahiertOracle LIKE Queries in DAO/BackofficeClaimSearchPort + SearchIndexAdapterRead Model, Adapterclaim-searchSearch Adapter TestsspĂ€ter OpenSearch möglichIndexkonsistenz
CLG-017CustomerCustomer Master Data entkoppeltDirekter SOAP Client und alte KundennummernCustomerMasterDataPort + CustomerSnapshotACL, Value Objectclaim-infrastructureCustomer Contract TestsDubletten/Inkonsistenz isoliertDatenqualitÀt bleibt Thema
CLG-018IdentityIdentity & Role Management abstrahiertJAAS/WebSphere-Rollen hart kodiertSecurityContextPort + AuthorizationPolicyPolicy Object, Role Mappingclaim-application, claim-infrastructureRole Mapping TestsIdP-Migration vorbereitetRollenmatrix abgleichen
CLG-019OutboxOutbox und Idempotency eingefĂŒhrtJMS/Payment im gleichen Fluss ohne WiederholschutzOutboxEvent + IdempotencyKeyOutbox Patternclaim-infrastructure, claim-paymentOutbox Publisher Testsrobuster bei Pod-RestartsEventual Consistency erklĂ€ren
CLG-020OpenShiftOpenShift-/Runtime-Migration vorbereitetEAR manuell auf WebSphere deploytContainer, Deployment, Route, ConfigMap, SecretRuntime Boundarymigration_to_openshiftDeployment Smoke TestsAutomatisierbare MigrationRuntime-Unterschiede prĂŒfen

AusfĂŒhrliche Hinweise

CLG-001 – Legacy-System als Vor-Refactoring-Ausgangspunkt sichtbar gemacht

CLG-002 – Fachlogik aus EJB-/SOAP-/JDBC-Schichten herausgezogen

CLG-003 – processClaimDecision(...) in fachliche Schritte zerlegt

CLG-004 – Claim Statuslogik aus if/else-Ketten herausgezogen

CLG-005 – Workflow Orchestrator eingefĂŒhrt

CLG-006 – Ports & Adapters eingefĂŒhrt

CLG-007 – SOAP Boundary kompatibel gehalten

CLG-008 – REST/OpenAPI ergĂ€nzt

CLG-009 – Document Adapter eingefĂŒhrt

CLG-010 – Fraud Risk Gateway entkoppelt

CLG-011 – Policy Coverage ĂŒber CoveragePort abstrahiert

CLG-012 – Payment Release entkoppelt

CLG-013 – NotificationPort modernisiert

CLG-014 – SLA & Escalation Scheduler getrennt

CLG-015 – Audit & Compliance Boundary getrennt

CLG-016 – Search Indexer ĂŒber ClaimSearchPort abstrahiert

CLG-017 – Customer Master Data entkoppelt

CLG-018 – Identity & Role Management abstrahiert

CLG-019 – Outbox und Idempotency eingefĂŒhrt

CLG-020 – OpenShift-/Runtime-Migration vorbereitet

12. Anhang D - Before/After Mapping

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang D - Before/After Mapping in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

BEFORE/AFTER-MAPPING

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

Legacy-Datei / ModulNeue Datei / neues ModulGrundPattern / ArchitekturkonzeptTest / SicherheitsnetzMigrationsnutzen
LegacyClaimFacadeBean.javaProcessClaimDecisionHandler.javaMonster Method zerlegtCommand Handler, Application ServiceGolden Master + Handler TestFachlogik kann unabhÀngig von WebSphere getestet und migriert werden
LegacyClaimStatusIfElse.javaClaimState.java + ClaimTransitionPolicy.javaStatuslogik aus if/else-Ketten herausgezogenState Pattern, Policy ObjectState Transition TestWorkflow-Regeln werden verstÀndlich und testbar
LegacyDocumentShareClient.javaDocumentStoragePort.java + FileShareDocumentAdapter.javaFile Share entkoppeltPort, Adapter, Anti-Corruption LayerAdapter TestDokumentenablage kann spÀter ersetzt werden
LegacyPartnerPortalServlet.javaPartnerPortalRestController.java / PartnerPortalBffAdapter.javaPartner Portal vom EJB getrenntStrangler UI, BFF AdapterPortal Contract TestExternes Portal kann schrittweise modernisiert werden
InternalClaimsBackofficeBean.javaClaimBackofficeUseCase.javaBackoffice-Logik aus JSP/EJB herausgezogenUse Case, Application ServiceBackoffice Use Case TestInterne UI kann spÀter Angular/React nutzen
FraudRiskSoapClient.javaFraudRiskPort.java + FraudRiskSoapAdapter.javaFraud SOAP entkoppeltPort, Adapter, ACLFraud Contract TestTimeout/Retry/Circuit Breaker möglich
PolicyCoverageSoapClient.javaCoveragePort.java + CoverageSoapAdapter.javaCoverage-System isoliertSpecification, Policy Object, AdapterCoverage Decision TestAlte Tariflogik kontrolliert migrierbar
PaymentReleaseSoapClient.javaPaymentReleasePort.java + PaymentReleaseSoapAdapter.javaPayment-Freigabe entkoppeltPort, Adapter, IdempotencyPayment Idempotency TestZahlungsrisiken werden reduziert
LegacyNotificationSenderBean.javaNotificationPort.java + NotificationAdapter.javaBenachrichtigung entkoppeltOutbox, AdapterNotification Retry TestDLQ/Retry auf Plattformebene möglich
SlaEscalationTimerBean.javaEscalationBatchJob.java + EscalationPolicy.javaEJB Timer ersetztBatch Worker, Policy Object, Lease LockBatch Lease TestOpenShift CronJob möglich
AuditReportStoredProcedureDao.javaAuditLogPort.java + AuditReportingAdapter.javaAudit/Reporting getrenntAudit Event, Reporting Read ModelAudit Trail TestCompliance-Berichte werden fachlich nachvollziehbar
LegacyClaimSearchDao.javaClaimSearchPort.java + SearchIndexAdapter.javaDB-LIKE-Suche entkoppeltRead Model, AdapterSearch Adapter TestspÀter Elasticsearch/OpenSearch möglich
CustomerMasterDataSoapClient.javaCustomerMasterDataPort.java + CustomerMasterDataSoapAdapter.javaCustomer-Stammdaten entkoppeltACL, Snapshot Value ObjectCustomer Contract TestDatenqualitÀtsprobleme werden isoliert
JaasRoleChecker.javaSecurityContextPort.java + AuthorizationPolicy.javaRollenprĂŒfung zentralisiertPolicy Object, Role MappingSecurity Role Mapping TestOpenShift/IdP-Migration vorbereitet
LegacyClaimEventPublisher.javaOutboxEvent.java + OutboxPublisherWorker.javaDirekte JMS-Publizierung abgelöstOutbox PatternOutbox Replay Testrobust gegen Transaktions- und Pod-Fehler
LegacyClaimDao.javaClaimRepository.java + JpaClaimRepository.javaDAO und Fachmodell getrenntRepository, Data MapperRepository Integration TestDB-Zugriff ist austauschbar
ClaimDecisionSoapEndpoint.javaClaimDecisionSoapEndpoint.java + ProcessClaimDecisionHandler.javaSOAP bleibt, delegiert aber nurFacade, ACLSOAP Compatibility ITPartner bleiben kompatibel
LegacyAuditLogger.javaAuditLogPort.java + StructuredAuditLogAdapter.javaversteckte Auditlogik sichtbar gemachtPort, Structured LoggingAudit Snapshot TestAudit kann zentral ausgewertet werden

Didaktische Lesart

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

13. Anhang E - Migration Decisions

Kapitelziel

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

MIGRATION_DECISIONS

14. Anhang F - Risks

Kapitelziel

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

RISKS

RisikoBeschreibungGegenmaßnahme
VerhaltensÀnderungRefactoring verÀndert unbemerkt Legacy-VerhaltenGolden Master und Characterization Tests
Doppelte ZahlungenPayment-Retry löst extern mehrfach ausIdempotencyKey, Outbox, Contract Tests
RollenfehlerJAAS-Gruppen werden falsch gemapptRole Mapping Test, Vier-Augen-PrĂŒfung
Batch-Duplikatemehrere Pods starten denselben JobLease Lock, CronJob Policy
DatenqualitÀtCustomer Master Data bleibt inkonsistentCustomerSnapshot, ACL, QualitÀtsreports

15. Anhang G - Runde 07 Codebeispiele

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang G - Runde 07 Codebeispiele in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Runde 7 – zentrale Codebeispiele

State/Policy statt if/else

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

public final class ClaimTransitionPolicy {
    public void assertAllowed(ClaimStatus current, ClaimDecisionType decision) {
        if (current == ClaimStatus.CLOSED) {
            throw new ClaimTransitionNotAllowedException("Closed claims cannot be changed");
        }
        if (decision == ClaimDecisionType.APPROVE_PAYMENT && current != ClaimStatus.DOCUMENTS_VERIFIED) {
            throw new ClaimTransitionNotAllowedException("Payment approval requires verified documents");
        }
    }
}

Command Handler als neue Use-Case-Grenze

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

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

    public ProcessClaimDecisionResult handle(ProcessClaimDecisionCommand command) {
        Claim claim = repository.getRequired(command.claimId());
        transitionPolicy.assertAllowed(claim.status(), command.decisionType());
        var documentDecision = documentVerificationPort.verify(command.claimId(), command.documentIds());
        var risk = fraudRiskPort.calculateRisk(command.claimId(), claim.customerId());
        var coverage = coveragePort.checkCoverage(claim.policyId(), command.claimId());
        claim.applyDecision(command.decisionType(), documentDecision, risk, coverage);
        repository.save(claim);
        auditLogPort.recordDecision(command.claimId(), command.actor(), command.decisionType());
        return ProcessClaimDecisionResult.from(claim);
    }
}

Mapping-Test gegen Legacy-Verhalten

package com.seb4u.demo.claims.migrationtests;

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

16. Anhang H - Großes Glossar

Kapitelziel

Dieses Hauptkapitel bĂŒndelt das zugehörige Rohmaterial zum Thema Anhang H - Großes Glossar in einer einheitlichen Struktur. Die fachlichen Inhalte, Codebeispiele und Tabellen bleiben erhalten; nur die Überschriftenebene wurde vereinfacht.

Dieses Glossar ist bewusst als großer Nachschlage-Anhang aufgebaut. Es erklĂ€rt zentrale Begriffe aus Legacy-Analyse, Refactoring, moderner Zielarchitektur, Zusatzsystemen, OpenShift-Migration und Claims-Fachlichkeit. Die EintrĂ€ge sind nicht als Ersatz fĂŒr die Kapitel gedacht, sondern als schnelle Orientierung beim Lesen des Lernbuchs.

Hinweis: Die Begriffe sind in Kategorien gruppiert. Unterpunkte innerhalb des Anhangs sind keine eigenen Klappkapitel, sondern normale Abschnitte innerhalb des Glossars.

Legacy-Analyse und Ausgangssystem

Legacy-System

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

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

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

IBM WebSphere Traditional

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

Beispiel im Lernprojekt: Ausgangspunkt der Migration.

Warum wichtig: Trennung von LaufzeitabhÀngigkeiten und Fachlogik.

EAR

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

Beispiel im Lernprojekt: legacy-websphere-ear.

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

WAR

ErklĂ€rung: Web Archive fĂŒr WeboberflĂ€chen, Servlets, JSPs und webnahe Ressourcen.

Beispiel im Lernprojekt: Partner Portal und Internal Backoffice.

Warum wichtig: UI-Schicht von Fachlogik trennen.

EJB-JAR

ErklĂ€rung: Archiv fĂŒr Enterprise JavaBeans, hĂ€ufig mit Transaktionen, Security, Scheduler und Fachlogik vermischt.

Beispiel im Lernprojekt: LegacyClaimFacadeBean.

Warum wichtig: Use Cases aus Container-Code herausziehen.

EJB

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

Beispiel im Lernprojekt: processClaimDecision in einem Session Bean.

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

JSP

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

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

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

Servlet

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

Beispiel im Lernprojekt: LegacyPartnerPortalServlet.

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

SOAP

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

Beispiel im Lernprojekt: createClaim, getClaimStatus, processClaimDecision.

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

WSDL

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

Beispiel im Lernprojekt: Claims SOAP API.

Warum wichtig: Contract Tests schĂŒtzen bestehende Partner.

JMS

ErklĂ€rung: Java Message Service fĂŒr asynchrone Nachrichten und Events.

Beispiel im Lernprojekt: ClaimDecisionProcessedEvent an IBM MQ.

Warum wichtig: Outbox entkoppelt Eventversand von Fachtransaktion.

IBM MQ

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

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

Warum wichtig: Adapter und Outbox reduzieren direkte Kopplung.

JTA

ErklĂ€rung: Java Transaction API fĂŒr containerverwaltete Transaktionen.

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

Warum wichtig: Transaktionsgrenzen werden bewusst gemacht.

JNDI

ErklĂ€rung: Namensdienst fĂŒr DataSources, Queues und Ressourcen in Application Servern.

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

Warum wichtig: Externe Konfiguration ersetzt servergebundene Lookups.

JNDI DataSource

ErklÀrung: Serverseitig konfigurierte Datenbankverbindung.

Beispiel im Lernprojekt: Oracle DataSource in WebSphere.

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

JAAS

ErklĂ€rung: Java Authentication and Authorization Service fĂŒr klassische Rollen- und SicherheitsprĂŒfung.

Beispiel im Lernprojekt: JaasRoleChecker.

Warum wichtig: SecurityContextPort und AuthorizationPolicy ersetzen harte PrĂŒfungen.

LDAP

ErklĂ€rung: Verzeichnisdienst fĂŒr Benutzer, Gruppen und Rollen.

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

Warum wichtig: Rollenmapping wird dokumentiert und testbar.

Stored Procedure

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

Beispiel im Lernprojekt: SP_PROCESS_CLAIM_DECISION.

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

Oracle LIKE Query

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

Beispiel im Lernprojekt: LegacyClaimSearchDao.

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

File Share

ErklĂ€rung: Gemeinsame Dateifreigabe fĂŒr Dokumente, hĂ€ufig mit harten Pfaden und schwacher Fehlerbehandlung.

Beispiel im Lernprojekt: LegacyDocumentShareClient.

Warum wichtig: DocumentStoragePort kapselt die Ablage.

Refactoring-Grundlagen

Refactoring

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

Beispiel im Lernprojekt: Zerlegung von processClaimDecision.

Warum wichtig: Risikoarme Modernisierung vor Plattformwechsel.

Characterization Test

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

Beispiel im Lernprojekt: LegacyProcessClaimDecisionCharacterizationTest.

Warum wichtig: Sicherheitsnetz vor dem Umbau.

Golden Master Test

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

Beispiel im Lernprojekt: GoldenMasterSnapshot.

Warum wichtig: Erkennt unbeabsichtigte VerhaltensÀnderungen.

Refactoring Seam

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

Beispiel im Lernprojekt: Wrapper um LegacyClaimFacadeBean.

Warum wichtig: Erlaubt inkrementelle Änderungen.

Monster Method

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

Beispiel im Lernprojekt: processClaimDecision(...).

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

Long Parameter List

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

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

Warum wichtig: Command Object bĂŒndelt Eingaben.

Primitive Obsession

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

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

Warum wichtig: Value Objects machen Regeln explizit.

God Class

ErklÀrung: Klasse mit zu vielen Verantwortlichkeiten.

Beispiel im Lernprojekt: LegacyClaimFacadeBean.

Warum wichtig: Verantwortlichkeiten werden auf Module verteilt.

Hidden Side Effect

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

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

Warum wichtig: Ports und Tests machen Seiteneffekte sichtbar.

Manual Error Handling

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

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

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

Technical Exception as Business Flow

ErklÀrung: Technische Exceptions steuern fachliche Entscheidungen.

Beispiel im Lernprojekt: Timeout bedeutet angeblich Fraud Reject.

Warum wichtig: Anti-Corruption Layer ĂŒbersetzt sauber.

Test Data Builder

ErklÀrung: Testhilfe zum lesbaren Erzeugen komplexer Testobjekte.

Beispiel im Lernprojekt: ClaimBuilder, CommandBuilder.

Warum wichtig: Tests werden kĂŒrzer und aussagekrĂ€ftiger.

Fake Object

ErklÀrung: Einfache Testimplementierung einer AbhÀngigkeit.

Beispiel im Lernprojekt: FakeFraudRiskPort.

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

Spy

ErklÀrung: Testobjekt, das Aufrufe aufzeichnet.

Beispiel im Lernprojekt: SpyAuditLogPort.

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

Adapter Test

ErklĂ€rung: Test fĂŒr eine technische Adapterimplementierung.

Beispiel im Lernprojekt: FraudRiskSoapAdapterTest.

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

Contract Test

ErklÀrung: Test gegen ein vereinbartes Schnittstellenverhalten.

Beispiel im Lernprojekt: SOAP Contract Test fĂŒr Partner.

Warum wichtig: SchĂŒtzt externe Konsumenten.

Architecture Test

ErklĂ€rung: Automatisierter Test fĂŒr Architekturregeln.

Beispiel im Lernprojekt: Domain darf Infrastructure nicht importieren.

Warum wichtig: Verhindert RĂŒckfall in enge Kopplung.

ArchUnit

ErklĂ€rung: Java-Testbibliothek zur PrĂŒfung von Paket- und Architekturregeln.

Beispiel im Lernprojekt: ArchitectureBoundaryTest.

Warum wichtig: Macht Architekturgrenzen dauerhaft prĂŒfbar.

Behavior Preservation

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

Beispiel im Lernprojekt: Legacy-vs-Modular Vergleichstest.

Warum wichtig: Wichtig fĂŒr risikoreiches Refactoring.

Incremental Refactoring

ErklĂ€rung: Umbau in kleinen, ĂŒberprĂŒfbaren Schritten statt Big Bang.

Beispiel im Lernprojekt: Phasen 1 bis 14.

Warum wichtig: Reduziert Migrationsrisiko.

Moderne Architektur und Domain Design

Hexagonale Architektur

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

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

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

Ports & Adapters

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

Beispiel im Lernprojekt: CoveragePort + CoverageSoapAdapter.

Warum wichtig: Integration austauschbar und testbar.

Port

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

Beispiel im Lernprojekt: PaymentReleasePort.

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

Adapter

ErklÀrung: Technische Implementierung eines Ports.

Beispiel im Lernprojekt: FileShareDocumentAdapter.

Warum wichtig: Kapselt alte Technik und Mapping.

Anti-Corruption Layer

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

Beispiel im Lernprojekt: FraudRiskSoapAdapter ĂŒbersetzt XML in RiskScore.

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

Domain Model

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

Beispiel im Lernprojekt: Claim, ClaimStatus, RiskScore.

Warum wichtig: Fachlichkeit wird zentral und verstÀndlich.

Entity

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

Beispiel im Lernprojekt: Claim.

Warum wichtig: Status und Entscheidungen werden konsistent verwaltet.

Value Object

ErklĂ€rung: UnverĂ€nderliches Objekt, das einen fachlichen Wert ausdrĂŒckt.

Beispiel im Lernprojekt: ClaimId, Money, RiskScore.

Warum wichtig: Validierung und Bedeutung liegen am richtigen Ort.

Aggregate

ErklÀrung: Konsistenzgrenze im Domain Model.

Beispiel im Lernprojekt: Claim mit Entscheidungen und Dokumentverweisen.

Warum wichtig: SchĂŒtzt Invarianten bei Änderungen.

Repository Pattern

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

Beispiel im Lernprojekt: ClaimRepository.

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

Application Service

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

Beispiel im Lernprojekt: ProcessClaimDecisionHandler.

Warum wichtig: Ersetzt EJB-Monsterlogik.

Command Object

ErklĂ€rung: Objekt, das Eingaben eines Use Cases bĂŒndelt.

Beispiel im Lernprojekt: ProcessClaimDecisionCommand.

Warum wichtig: Ersetzt lange Parameterlisten.

Result Object

ErklĂ€rung: Objekt, das fachliches Ergebnis und Folgeinformationen strukturiert zurĂŒckgibt.

Beispiel im Lernprojekt: ProcessClaimDecisionResult.

Warum wichtig: Ersetzt technische Returncodes und Exceptions.

Policy Object

ErklÀrung: Kapselt eine fachliche Regel.

Beispiel im Lernprojekt: AuthorizationPolicy, NotificationTemplatePolicy.

Warum wichtig: Regeln sind einzeln testbar.

State Pattern

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

Beispiel im Lernprojekt: ClaimTransitionPolicy.

Warum wichtig: StatusĂŒbergĂ€nge werden verstĂ€ndlich.

Specification Pattern

ErklĂ€rung: Kombinierbare fachliche PrĂŒfregeln.

Beispiel im Lernprojekt: CoverageSpecification.

Warum wichtig: Komplexe Regeln werden modular.

Facade Pattern

ErklÀrung: Vereinfachte Schnittstelle vor komplexem Subsystem.

Beispiel im Lernprojekt: LegacyClaimFacade als Übergangspunkt.

Warum wichtig: Ermöglicht erste sichere Schnittlinie.

Workflow Orchestrator

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

Beispiel im Lernprojekt: ClaimDecisionWorkflowOrchestrator.

Warum wichtig: Workflow wird nachvollziehbar und testbar.

Approval Chain

ErklĂ€rung: Genehmigungskette fĂŒr Entscheidungen, Zahlungen oder Eskalationen.

Beispiel im Lernprojekt: Teamlead Approval vor Payment Release.

Warum wichtig: Ersetzt verstreute RollenprĂŒfungen.

Shared Kernel

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

Beispiel im Lernprojekt: shared-kernel Modul.

Warum wichtig: Verhindert Duplikate und unklare Basistypen.

Bounded Context

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

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

Warum wichtig: Vermeidet ein ĂŒbergroßes gemeinsames Modell.

Zusatzsysteme und Integrationen

Partner Portal

ErklĂ€rung: Externes Portal fĂŒr Partner wie WerkstĂ€tten, Makler oder Schadensmelder.

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

Warum wichtig: Kann ĂŒber REST/BFF stranguliert werden.

Internal Backoffice

ErklĂ€rung: Interne Anwendung fĂŒr Sachbearbeiter, Teamleiter und Spezialisten.

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

Warum wichtig: Backoffice API entkoppelt UI und Use Cases.

Document Management Adapter

ErklĂ€rung: Adapter zur Ablage, PrĂŒfung, Versionierung und Archivierung von Dokumenten.

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

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

Document Storage Port

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

Beispiel im Lernprojekt: DocumentStoragePort.

Warum wichtig: Ablage ist austauschbar.

Document Verification Port

ErklĂ€rung: Schnittstelle zur fachlichen DokumentenprĂŒfung.

Beispiel im Lernprojekt: DocumentVerificationPort.

Warum wichtig: SOAP-PrĂŒfdienst wird entkoppelt.

Fraud Risk Gateway

ErklĂ€rung: Integration zur BetrugsrisikoprĂŒfung.

Beispiel im Lernprojekt: FraudRiskSoapClient.

Warum wichtig: RiskScore wird fachlich modelliert.

Fraud Risk Port

ErklÀrung: Fachlicher Port zur Risikoabfrage.

Beispiel im Lernprojekt: FraudRiskPort.

Warum wichtig: Timeouts und XML bleiben im Adapter.

Policy Coverage System

ErklĂ€rung: System zur PrĂŒfung der Versicherungsdeckung.

Beispiel im Lernprojekt: PolicyCoverageSoapClient.

Warum wichtig: CoverageDecision ersetzt technische Antwortformate.

Coverage Port

ErklĂ€rung: Fachliche Schnittstelle zur DeckungsprĂŒfung.

Beispiel im Lernprojekt: CoveragePort.

Warum wichtig: Regeln werden test- und austauschbar.

Payment Release System

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

Beispiel im Lernprojekt: PaymentReleaseSoapClient.

Warum wichtig: PaymentReleasePort plus Idempotency schĂŒtzt vor Doppelzahlung.

Payment Release Port

ErklÀrung: Schnittstelle zum Zahlungssystem.

Beispiel im Lernprojekt: PaymentReleasePort.

Warum wichtig: Kompensation und Fehlerbehandlung werden kontrolliert.

Notification System

ErklĂ€rung: System fĂŒr E-Mail, SMS, interne Nachrichten und Templates.

Beispiel im Lernprojekt: LegacyNotificationSenderBean.

Warum wichtig: NotificationPort entkoppelt Benachrichtigung.

Notification Port

ErklÀrung: Fachlicher Port zum Versand von Benachrichtigungen.

Beispiel im Lernprojekt: NotificationPort.

Warum wichtig: Outbox und Retry werden möglich.

SLA & Escalation Scheduler

ErklĂ€rung: Scheduler fĂŒr Fristen, Eskalationen und SLA-Verletzungen.

Beispiel im Lernprojekt: SlaEscalationTimerBean.

Warum wichtig: OpenShift CronJob und EscalationPolicy ersetzen TimerBean.

Audit & Compliance Reporter

ErklĂ€rung: System fĂŒr Nachvollziehbarkeit, Reports und Compliance-Auswertungen.

Beispiel im Lernprojekt: AuditReportStoredProcedureDao.

Warum wichtig: AuditLogPort und Reporting Read Model trennen Verantwortung.

Search Indexer

ErklĂ€rung: Suchkomponente fĂŒr Claims, Dokumente, PartnerfĂ€lle oder Kunden.

Beispiel im Lernprojekt: LegacyClaimSearchDao.

Warum wichtig: ClaimSearchPort trennt SuchdomÀne.

Customer Master Data System

ErklĂ€rung: Stammdatensystem fĂŒr Kundendaten, Adressen und Segmente.

Beispiel im Lernprojekt: CustomerMasterDataSoapClient.

Warum wichtig: CustomerSnapshot stabilisiert Daten.

Identity & Role Management

ErklĂ€rung: System fĂŒr Benutzer, Gruppen, Rollen und Berechtigungen.

Beispiel im Lernprojekt: JaasRoleChecker.

Warum wichtig: SecurityContextPort + AuthorizationPolicy trennen Security vom Fachcode.

Messaging, Transaktionen und Resilienz

Outbox Pattern

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

Beispiel im Lernprojekt: OutboxEvent nach Claim Decision.

Warum wichtig: Verhindert verlorene Events.

Idempotency

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

Beispiel im Lernprojekt: Payment Idempotency Key.

Warum wichtig: SchĂŒtzt bei Retry und Netzwerkfehlern.

Retry Policy

ErklĂ€rung: Regel fĂŒr Wiederholungsversuche technischer Aufrufe.

Beispiel im Lernprojekt: Notification Retry.

Warum wichtig: Fehlerbehandlung wird explizit.

Circuit Breaker

ErklĂ€rung: Schaltet instabile externe Systeme vorĂŒbergehend ab.

Beispiel im Lernprojekt: FraudRiskService Timeout.

Warum wichtig: Verhindert KaskadenausfÀlle.

Timeout Policy

ErklÀrung: Begrenzt Wartezeit auf externe Systeme.

Beispiel im Lernprojekt: SOAP-Aufruf zum Coverage System.

Warum wichtig: Threads und Transaktionen werden geschĂŒtzt.

Dead Letter Queue

ErklĂ€rung: Ablage fĂŒr Nachrichten, die nicht erfolgreich verarbeitet werden konnten.

Beispiel im Lernprojekt: Notification DLQ.

Warum wichtig: Fehler bleiben sichtbar und analysierbar.

Compensation

ErklÀrung: Ausgleichsaktion nach teilweisem Erfolg.

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

Warum wichtig: Ersetzt unrealistische globale Transaktionen.

Exactly Once Illusion

ErklĂ€rung: Irrtum, dass verteilte Systeme immer exakt einmal ausfĂŒhren können.

Beispiel im Lernprojekt: Payment und Notification.

Warum wichtig: Idempotency und Outbox sind realistischer.

At-Least-Once Delivery

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

Beispiel im Lernprojekt: Outbox Publisher.

Warum wichtig: Consumer mĂŒssen idempotent sein.

Transactional Boundary

ErklÀrung: Bewusste Grenze einer Transaktion.

Beispiel im Lernprojekt: Claim speichern + Outbox schreiben.

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

Eventual Consistency

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

Beispiel im Lernprojekt: Search Read Model nach Claim Update.

Warum wichtig: Erlaubt entkoppelte Verarbeitung.

Correlation ID

ErklĂ€rung: Kennung zur Verfolgung eines Vorgangs ĂŒber Systemgrenzen.

Beispiel im Lernprojekt: Claim Decision Request.

Warum wichtig: Verbessert Debugging und Audit.

Lease Lock

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

Beispiel im Lernprojekt: EscalationBatchJob.

Warum wichtig: Kubernetes-Skalierung bleibt kontrolliert.

OpenShift, Container und Betrieb

OpenShift

ErklĂ€rung: Enterprise-Kubernetes-Plattform von Red Hat fĂŒr Build, Deployment, Security und Betrieb.

Beispiel im Lernprojekt: Zielplattform.

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

Containerfile

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

Beispiel im Lernprojekt: Containerfile.openliberty.

Warum wichtig: Runtime wird reproduzierbar.

Open Liberty

ErklĂ€rung: Leichtgewichtige Java-Enterprise-Runtime fĂŒr containerisierte Workloads.

Beispiel im Lernprojekt: Open Liberty Zielruntime.

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

Spring Boot

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

Beispiel im Lernprojekt: Alternative Zielruntime.

Warum wichtig: Erlaubt moderne REST- und Betriebsendpunkte.

ConfigMap

ErklĂ€rung: Kubernetes/OpenShift-Objekt fĂŒr nicht-geheime Konfiguration.

Beispiel im Lernprojekt: Feature Flags, URLs, Timeouts.

Warum wichtig: Ersetzt harte Konfiguration im Code.

Secret

ErklĂ€rung: Kubernetes/OpenShift-Objekt fĂŒr sensible Konfiguration.

Beispiel im Lernprojekt: DB-Passwort, SOAP Credentials.

Warum wichtig: Keine Passwörter im Code.

Route

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

Beispiel im Lernprojekt: claim-rest-api Route.

Warum wichtig: Kontrollierter Zugriff von außen.

Service

ErklĂ€rung: Kubernetes-Abstraktion fĂŒr stabile Netzwerkadresse zu Pods.

Beispiel im Lernprojekt: claim-service.

Warum wichtig: Pods können ersetzt werden.

Deployment

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

Beispiel im Lernprojekt: claim-service Deployment.

Warum wichtig: Reproduzierbare Releases.

CronJob

ErklĂ€rung: Kubernetes/OpenShift-Ressource fĂŒr geplante Jobs.

Beispiel im Lernprojekt: Escalation CronJob.

Warum wichtig: Ersetzt EJB Timer.

Liveness Probe

ErklĂ€rung: PrĂŒft, ob ein Container noch lebt.

Beispiel im Lernprojekt: Health Endpoint.

Warum wichtig: Defekte Pods werden neu gestartet.

Readiness Probe

ErklĂ€rung: PrĂŒft, ob ein Pod bereit ist, Traffic anzunehmen.

Beispiel im Lernprojekt: DB- und Dependency Check.

Warum wichtig: Verhindert Traffic auf nicht bereite Pods.

Startup Probe

ErklĂ€rung: PrĂŒft den Startvorgang langsamer Anwendungen.

Beispiel im Lernprojekt: Open Liberty Start.

Warum wichtig: Verhindert zu frĂŒhe Neustarts.

HPA

ErklÀrung: Horizontal Pod Autoscaler.

Beispiel im Lernprojekt: Skalierung nach CPU oder Metriken.

Warum wichtig: Betrieb reagiert auf Last.

PDB

ErklÀrung: PodDisruptionBudget.

Beispiel im Lernprojekt: MindestverfĂŒgbarkeit bei Wartung.

Warum wichtig: SchĂŒtzt gegen zu viele gleichzeitige AusfĂ€lle.

NetworkPolicy

ErklĂ€rung: Kubernetes-Regel fĂŒr Netzwerkzugriffe zwischen Pods.

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

Warum wichtig: Reduziert AngriffsflÀche.

Observability

ErklĂ€rung: Sammelbegriff fĂŒr Logs, Metriken, Traces und Health.

Beispiel im Lernprojekt: Structured Logs, Metrics, Tracing.

Warum wichtig: Betrieb wird nachvollziehbar.

Structured Logging

ErklÀrung: Logs mit Feldern statt nur Textzeilen.

Beispiel im Lernprojekt: claimId, correlationId, decision.

Warum wichtig: Bessere Suche und Analyse.

Metrics

ErklĂ€rung: Messwerte ĂŒber Laufzeit und Verhalten.

Beispiel im Lernprojekt: Decision Duration, Error Count.

Warum wichtig: Monitoring und Alerting werden möglich.

Tracing

ErklĂ€rung: Verfolgung eines Requests ĂŒber Service- und Systemgrenzen.

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

Warum wichtig: Hilft bei Fehleranalyse.

Dokumentation, Change Management und Migration

Before/After Mapping

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

Beispiel im Lernprojekt: LegacyClaimFacadeBean -> ProcessClaimDecisionHandler.

Warum wichtig: Migrationsnutzen wird nachvollziehbar.

Changelog V2

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

Beispiel im Lernprojekt: CHANGELOG_V2.

Warum wichtig: ArchitekturÀnderungen bleiben erklÀrbar.

Change Matrix

ErklĂ€rung: Tabellarische Übersicht ĂŒber Änderungen und betroffene Module.

Beispiel im Lernprojekt: CHANGELOG_V2_MATRIX.csv.

Warum wichtig: Hilft bei Review und Audit.

Migration Decision

ErklĂ€rung: Dokumentierte Entscheidung mit Kontext, Option, BegrĂŒndung und Risiko.

Beispiel im Lernprojekt: MIGRATION_DECISIONS.

Warum wichtig: Verhindert verlorenes Architekturwissen.

Risk Register

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

Beispiel im Lernprojekt: RISKS.

Warum wichtig: Macht Migration planbarer.

Rollback Plan

ErklĂ€rung: Plan zur RĂŒckkehr auf einen sicheren Zustand.

Beispiel im Lernprojekt: Cutover mit RĂŒckfalloption.

Warum wichtig: Reduziert Produktionsrisiko.

Cutover

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

Beispiel im Lernprojekt: SOAP Boundary parallel betreiben.

Warum wichtig: Migration wird kontrolliert.

Strangler Pattern

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

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

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

Parallel Run

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

Beispiel im Lernprojekt: Legacy-vs-Modular Vergleich.

Warum wichtig: Erhöht Sicherheit vor Abschaltung.

Smoke Test

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

Beispiel im Lernprojekt: REST Smoke IT.

Warum wichtig: Findet grobe Deploy-Fehler frĂŒh.

Migration Test

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

Beispiel im Lernprojekt: OpenShiftDeploymentSmokeIT.

Warum wichtig: Sichert Runtime- und Konfigurationswechsel.

Artifact Manifest

ErklÀrung: Maschinenlesbare Liste erzeugter Dateien und Pakete.

Beispiel im Lernprojekt: artifact_manifest.json.

Warum wichtig: Erleichtert PrĂŒfung des Gesamtpakets.

Link Check

ErklĂ€rung: PrĂŒfung interner Links und Anker.

Beispiel im Lernprojekt: link_check.

Warum wichtig: Verhindert kaputte Navigation.

PDF Check

ErklĂ€rung: PrĂŒfung von PDF-Seiten, Inhaltsverzeichnis, Bookmarks und Lesbarkeit.

Beispiel im Lernprojekt: pdf_check.

Warum wichtig: Sichert brauchbare Ausgabe.

Fachbegriffe im Claims-Kontext

Claim

ErklĂ€rung: Schadensfall oder Leistungsfall, der gemeldet, geprĂŒft, entschieden und ggf. bezahlt wird.

Beispiel im Lernprojekt: Claim Aggregate.

Warum wichtig: Zentrales fachliches Objekt.

Claim Status

ErklÀrung: Aktueller Zustand eines Schadensfalls.

Beispiel im Lernprojekt: CREATED, DOCUMENTS_PENDING, APPROVED.

Warum wichtig: Statuslogik wird ĂŒber Policies gesteuert.

Claim Decision

ErklĂ€rung: Fachliche Entscheidung ĂŒber den weiteren Verlauf eines Claims.

Beispiel im Lernprojekt: Approve, Reject, Escalate.

Warum wichtig: Use Case wird explizit modelliert.

Coverage

ErklĂ€rung: DeckungsprĂŒfung, ob Police und Schaden zusammenpassen.

Beispiel im Lernprojekt: CoverageDecision.

Warum wichtig: Externe Tariflogik wird entkoppelt.

Deductible

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

Beispiel im Lernprojekt: Coverage Calculation.

Warum wichtig: Fachwert statt bloßer Zahl.

Fraud Risk

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

Beispiel im Lernprojekt: RiskScore.

Warum wichtig: Technische Risikoantwort wird fachlich nutzbar.

Document Verification

ErklĂ€rung: PrĂŒfung von eingereichten Dokumenten auf VollstĂ€ndigkeit und GĂŒltigkeit.

Beispiel im Lernprojekt: verifyClaimDocument.

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

Payment Approval

ErklÀrung: Fachliche Freigabe einer Auszahlung.

Beispiel im Lernprojekt: approveClaimPayment.

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

Escalation

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

Beispiel im Lernprojekt: escalateClaim.

Warum wichtig: EscalationPolicy ersetzt TimerBean-Logik.

SLA

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

Beispiel im Lernprojekt: SLA & Escalation Scheduler.

Warum wichtig: Fristen werden testbar berechnet.

Audit Trail

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

Beispiel im Lernprojekt: getClaimAuditTrail.

Warum wichtig: Compliance wird nachvollziehbar.

Customer Snapshot

ErklÀrung: Stabile Momentaufnahme relevanter Kundendaten.

Beispiel im Lernprojekt: CustomerSnapshot Value Object.

Warum wichtig: SchĂŒtzt vor inkonsistenten Stammdaten.

Partner Identity

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

Beispiel im Lernprojekt: Partner Identity Service.

Warum wichtig: Partnerzugriffe werden kontrolliert.

⌂ Cockpit