SOAP Legacy Integration
SOAP-Anbindung als Anti-Corruption Layer statt Legacy-Kopplung im Domain-Code.
Workshop-Slice
Dieses Lab zeigt einen konkreten Enterprise-Fehler mit Vorher/Nachher-Code. Die Beispiele sind bewusst fachlich benannt, damit du Architekturentscheidungen und nicht nur Syntax übst.
Lernziel
Du kapselst SOAP-Legacy-Systeme hinter einem Port und schützt Domain und Use Cases vor WSDL-/XML-Details.
Ausgangslage
Ein Billing-Use-Case ruft direkt einen generierten SOAP-Client auf. Dadurch hängen Fachlogik, XML-Struktur, Timeout und Fehlerbehandlung zusammen.
Vorher: problematischer Code
Die generierten SOAP-Klassen sickern in die Application-Schicht. Fachliche Tests benötigen plötzlich SOAP-Stubs.
class BillingService {
private final LegacyBillingPortType soap;
void createInvoice(Order order) {
CreateInvoiceRequest request = new CreateInvoiceRequest();
request.setCustomerNo(order.customerId().value());
request.setAmount(order.total().asBigDecimal());
CreateInvoiceResponse response = soap.createInvoice(request);
if (!"OK".equals(response.getStatus())) {
throw new RuntimeException(response.getErrorText());
}
}
}
Analyse: Was ist daran schlecht?
- Application-Schicht kennt generierte SOAP-Klassen.
- Legacy-Statuscodes werden nicht fachlich übersetzt.
- Timeout/Retry-Verhalten ist nicht dokumentiert.
- Änderungen an der WSDL brechen fachliche Services.
Nachher: bessere Lösung
Der SOAP-Client sitzt in einem Adapter. Der Use Case spricht nur mit einem fachlichen Port.
// Pattern: Port
// Zweck: Application-Schicht kennt nur fachliche Operationen, nicht SOAP/XML.
public interface InvoicePort {
InvoiceId createInvoiceFor(ConfirmedOrder order);
}
// Pattern: Adapter / Anti-Corruption Layer
// Zweck: Legacy SOAP wird in ein modernes fachliches Modell übersetzt.
public final class SoapInvoiceAdapter implements InvoicePort {
private final LegacyBillingPortType soap;
private final SoapInvoiceMapper mapper;
public InvoiceId createInvoiceFor(ConfirmedOrder order) {
CreateInvoiceRequest request = mapper.toSoap(order);
CreateInvoiceResponse response = soap.createInvoice(request);
if ("DUPLICATE".equals(response.getStatus())) {
throw new InvoiceAlreadyExists(order.orderId());
}
if (!"OK".equals(response.getStatus())) {
throw new LegacyBillingUnavailable(response.getErrorText());
}
return new InvoiceId(response.getInvoiceNo());
}
}
legacy:
billing:
endpoint: https://billing-legacy.local/ws
connect-timeout: 2s
read-timeout: 5s
retry:
max-attempts: 2
retry-on: CONNECT_TIMEOUT,HTTP_503
Test / Prüfnachweis
Dieser Abschnitt zeigt, wie du die Verbesserung nachweist. Es ist bewusst kein reiner Happy-Path-Test, sondern prüft ein Risiko aus dem Vorher-Teil.
@Test
void mapsDuplicateStatusToDomainException() {
var soap = new FakeLegacyBillingPort("DUPLICATE", "Invoice exists");
var adapter = new SoapInvoiceAdapter(soap, new SoapInvoiceMapper());
assertThrows(InvoiceAlreadyExists.class,
() -> adapter.createInvoiceFor(confirmedOrder("O-77")));
}
Typische Fehler
- SOAP-Klassen im Domain-Modell verwenden.
- Jeden Legacy-Fehler als RuntimeException weiterreichen.
- Timeouts nur implizit über Container-Defaults lassen.
- XML-Felder 1:1 als interne API übernehmen.
Deep-Learning-Bezug
Die Links führen zum ausführlichen Inhalt; die Deep-Learning-Seite bleibt nur die Lernlandkarte und kopiert den Inhalt nicht doppelt.
Prüfcheckliste
- Use Case importiert keine SOAP-Klasse.
- Adapter übersetzt Legacy-Fehler fachlich.
- Timeouts sind konfiguriert und dokumentiert.
- Mapper enthält keine fachliche Entscheidung.