Enterprise SOAP Verständnis - Lehrbuch
Contract-First, WSDL/XSD, Jakarta/JAX-WS, Security, Fehler, Betrieb und Modernisierung
Ein praxisnahes deutsches Lehrbuch für Senior-Java-/Enterprise-Kontext: fachliche Verträge, technische Nachrichten, Code, Betrieb und Modernisierung.
Kompaktes Inhaltsverzeichnis
1. Worum es bei Enterprise SOAP wirklich geht
SOAP ist im Enterprise-Umfeld nicht nur ein altes XML-Protokoll. Entscheidend ist die Denkweise: Ein Service stellt einen stabilen, maschinenlesbaren Vertrag bereit. Dieser Vertrag beschreibt Nachrichten, Typen, Operationen, Fehler und den technischen Transport. Dadurch können ein Java-System, ein .NET-System, ein Mainframe-Gateway, ein ESB und ein Partnerportal miteinander sprechen, obwohl sie intern völlig unterschiedlich gebaut sind.
In vielen Firmen ist SOAP noch wichtig, weil Verträge über Jahre stabil bleiben müssen. Banken, Versicherungen, Behörden, Logistik, Telekommunikation und große Handelsplattformen haben häufig Partnerintegrationen, die nicht einfach jedes Quartal gebrochen werden dürfen. Ein REST-Endpunkt kann sehr modern sein, aber wenn hunderte externe Partner nach WSDL generierte Clients betreiben, ist Stabilität wichtiger als modischer Schnittstellenwechsel.
Die Grundregel lautet: SOAP lohnt sich besonders, wenn der Vertrag streng, versionierbar, typisiert und interoperabel sein muss. SOAP ist weniger angenehm für einfache Web-APIs, mobile Frontends oder schnelle Public APIs. In Enterprise-Landschaften wird SOAP oft dort behalten, wo es einen stabilen B2B-Vertrag, starke Security-Anforderungen, formale Schemas, asynchrone Middleware oder lange Lebenszyklen gibt.
Mentales Modell: REST denkt oft ressourcenorientiert. SOAP denkt nachrichten- und operationsorientiert. Eine SOAP-Operation ist fachlich meist ein Kommando oder eine Abfrage: SubmitOrder, CreateInvoice, CheckCreditLimit, GetPolicyStatus. Die XML-Nachricht ist dabei nicht zufälliger Payload, sondern Teil des Vertrages.
2. Fachliches Beispiel: Order & Billing im Großunternehmen
Wir verwenden im ganzen Lehrbuch ein einheitliches Beispiel: Ein Partnerportal sendet Bestellungen an ein internes Order-&-Billing-System. Der Partner darf Bestellungen anlegen, den Status abfragen und im Fehlerfall eine fachliche Rückmeldung erhalten. Intern werden Kundendaten, Preisregeln, Kreditlimit, Rechnung und Lieferung geprüft.
Die fachliche Sprache ist wichtiger als der erste Code. Bevor man WSDL schreibt, müssen Begriffe geklärt werden:
CustomerNumber: externe Kundennummer, nicht interne Datenbank-ID.PartnerOrderId: eindeutige Bestellnummer des Partners.IdempotencyKey: technische Absicherung gegen doppelte Verarbeitung.OrderLine: einzelne Position mit Artikel, Menge und Preisreferenz.BusinessFault: fachlicher Fehler, zum Beispiel unbekannter Kunde oder Kreditlimit überschritten.TechnicalFault: technischer Fehler, zum Beispiel Backend nicht erreichbar.
Ein typischer Enterprise-Fehler ist, technische Tabellenbegriffe direkt in den Vertrag zu schieben. Ein SOAP-Vertrag ist langlebig. Deshalb sollte er fachlich stabil sein und nicht die aktuelle Datenbankstruktur abbilden. Wenn heute T_CUSTOMER_XREF existiert und morgen ein MDM-System eingeführt wird, darf der externe Vertrag nicht unnötig brechen.
Fachliche Schnittstellenentscheidung: Der Partner sendet keine Datenbank-IDs. Er sendet PartnerOrderId, CustomerNumber und fachlich verständliche Positionen. Intern mappt der Order Service diese Informationen auf eigene Domänenobjekte.
3. Technische Anatomie einer SOAP-Nachricht
Eine SOAP-Nachricht besteht aus einem Envelope. Darin gibt es optional einen Header und verpflichtend einen Body. Der Header enthält technische Querschnittsdaten wie Security, Korrelations-ID, Routing-Informationen oder Zeitstempel. Der Body enthält die fachliche Nutzlast oder einen Fault.
Wichtig ist die Trennung: Header sind für Infrastruktur und Querschnittsfunktionen gedacht. Der Body ist für den fachlichen Vertrag gedacht. Natürlich kann man alles in den Body packen, aber dann vermischt man Fachlichkeit und Infrastruktur. Das erschwert Wiederverwendung, Security-Policies und Betrieb.
POST /services/OrderService HTTP/1.1
Host: api.company.example
Content-Type: text/xml; charset=utf-8
SOAPAction: "submitOrder"
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order/v1">
<soapenv:Header>
<ord:CorrelationId>c-2026-07-07-00042</ord:CorrelationId>
<ord:IdempotencyKey>partner-4711-20260707-1</ord:IdempotencyKey>
</soapenv:Header>
<soapenv:Body>
<ord:SubmitOrderRequest>
<ord:PartnerOrderId>PO-4711</ord:PartnerOrderId>
<ord:CustomerNumber>C-100200</ord:CustomerNumber>
<ord:OrderLine>
<ord:Sku>BOOK-JAVA-21</ord:Sku>
<ord:Quantity>3</ord:Quantity>
</ord:OrderLine>
</ord:SubmitOrderRequest>
</soapenv:Body>
</soapenv:Envelope>
SOAP 1.1 verwendet typischerweise text/xml; SOAP 1.2 verwendet den Media Type application/soap+xml. In Legacy-Systemen begegnet man sehr häufig SOAP 1.1, weil viele ältere JAX-WS-, WebSphere-, .NET- und ESB-Landschaften darauf standardisiert wurden. Für das Verständnis ist die Grundstruktur aber ähnlich: Envelope, Header, Body, Fault.
4. WSDL und XSD: Der Vertrag ist wichtiger als der Code
WSDL beschreibt, welche Operationen ein Web Service anbietet, welche Nachrichten ausgetauscht werden und wie der Service technisch erreichbar ist. XSD beschreibt die Datenstrukturen. Bei einem sauberen Contract-First-Ansatz beginnt man mit Fachsprache, XSD und WSDL. Daraus werden Java-Klassen, Clients und Server-Schnittstellen generiert oder daran ausgerichtet.
Die wichtigsten WSDL-Bausteine sind:
types: XSD-Schemas für Datenstrukturen.message: abstrakte Request-/Response-/Fault-Nachrichten.portType: fachliche Operationen, zum BeispielsubmitOrder.binding: konkrete technische Bindung, meistens SOAP über HTTP.service: konkreter Endpoint, also die Adresse.
<definitions name="OrderService"
targetNamespace="http://example.com/order/v1"
xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://example.com/order/v1"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
<types>
<xsd:schema targetNamespace="http://example.com/order/v1"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="SubmitOrderRequest" type="tns:SubmitOrderRequestType"/>
<xsd:element name="SubmitOrderResponse" type="tns:SubmitOrderResponseType"/>
</xsd:schema>
</types>
<message name="SubmitOrderInput">
<part name="parameters" element="tns:SubmitOrderRequest"/>
</message>
<message name="SubmitOrderOutput">
<part name="parameters" element="tns:SubmitOrderResponse"/>
</message>
<portType name="OrderPortType">
<operation name="submitOrder">
<input message="tns:SubmitOrderInput"/>
<output message="tns:SubmitOrderOutput"/>
</operation>
</portType>
<binding name="OrderSoapBinding" type="tns:OrderPortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="submitOrder">
<soap:operation soapAction="submitOrder"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
</binding>
</definitions>
In professionellen SOAP-Landschaften ist document/literal oder document/literal wrapped meist die beste Wahl. rpc/encoded sollte man in modernen Enterprise-Verträgen vermeiden, weil Interoperabilität, Tooling und Profilkonformität schwieriger werden.
5. XSD ausführlich: Datenvertrag, Pflichtfelder und Erweiterbarkeit
XSD entscheidet, welche Daten gültig sind. Dadurch verschiebt sich ein Teil der Validierung aus Java-Code in den Vertrag. Das ist gut, wenn die Regeln stabil und technisch eindeutig sind. Es ist schlecht, wenn man versucht, komplexe Geschäftsentscheidungen vollständig in XSD zu pressen.
Gute XSD-Regeln:
- Datentypen und Formate:
date,decimal,string,int. - Pflichtfelder:
minOccurs="1". - optionale Erweiterungen:
minOccurs="0". - Listen:
maxOccurs="unbounded"oder bewusste Obergrenze. - einfache Codes:
enumeration, wenn die Liste wirklich stabil ist.
Schlechte XSD-Regeln:
- Kreditprüfung als riesige Schema-Logik.
- dynamische Produktregeln.
- Regeln, die sich wöchentlich ändern.
- interne Datenbankdetails.
<xsd:complexType name="SubmitOrderRequestType">
<xsd:sequence>
<xsd:element name="PartnerOrderId" type="tns:ExternalIdType"/>
<xsd:element name="CustomerNumber" type="tns:CustomerNumberType"/>
<xsd:element name="RequestedDeliveryDate" type="xsd:date" minOccurs="0"/>
<xsd:element name="OrderLine" type="tns:OrderLineType" maxOccurs="100"/>
<xsd:element name="Comment" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ExternalIdType">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1"/>
<xsd:maxLength value="80"/>
<xsd:pattern value="[A-Za-z0-9_.\-]+"/>
</xsd:restriction>
</xsd:simpleType>
Enterprise-Regel: XSD validiert die Nachricht, nicht die gesamte Fachlichkeit. Wenn ein Kunde unbekannt ist, ist die XML-Struktur trotzdem gültig. Dann antwortet der Service mit einem fachlichen Fault oder einer fachlichen Response, nicht mit einem Schemafehler.
6. Contract-First statt Code-First
Code-First wirkt am Anfang schneller: Man schreibt eine Java-Klasse, annotiert sie und lässt daraus WSDL erzeugen. Das Problem entsteht später. Java-Namen, Paketstrukturen, Datentypen und Annotationen werden plötzlich Teil eines externen Vertrags. Eine kleine Refactoring-Änderung kann versehentlich den Vertrag verändern.
Contract-First ist langsamer beim Start, aber stabiler im Enterprise-Betrieb. Der Vertrag wird bewusst designt, reviewt, versioniert und getestet. Java-Code ist dann eine Implementierung des Vertrags, nicht die Quelle der Wahrheit.
Contract-First-Ablauf:
1. Fachliche Begriffe klären. 2. XSD-Datenvertrag schreiben. 3. WSDL-Operationen und Faults definieren. 4. Artefakte generieren oder manuell daran ausrichten. 5. Contract Tests und Beispielnachrichten versionieren. 6. Erst dann Implementierung bauen.
// Contract-First: Dieses Interface folgt dem WSDL-Vertrag.
// Pattern: Facade - nach außen wird eine stabile Service-Fassade angeboten.
@WebService(
name = "OrderPortType",
targetNamespace = "http://example.com/order/v1"
)
public interface OrderPortType {
@WebMethod(operationName = "submitOrder")
SubmitOrderResponse submitOrder(SubmitOrderRequest request)
throws BusinessFault, TechnicalFault;
}
In Legacy-Systemen findet man oft einen Mischzustand: Ursprünglich Code-First erzeugt, dann manuell erweitert, dann von einem ESB transformiert. Beim Modernisieren sollte man zuerst den tatsächlich genutzten Vertrag rekonstruieren und stabilisieren, bevor man eine neue Implementierung baut.
7. Java/Jakarta Endpoint: Service-Fassade, Mapper und Domänenservice
Ein SOAP-Endpoint sollte nicht die gesamte Fachlogik enthalten. Er ist eine Fassade. Er nimmt die SOAP-Nachricht entgegen, validiert technische Rahmenbedingungen, mappt DTOs auf Domänenobjekte, ruft Anwendungsservices auf und mappt das Ergebnis zurück.
package com.example.soap;
import jakarta.jws.WebService;
import jakarta.xml.ws.BindingType;
import jakarta.xml.ws.soap.SOAPBinding;
@WebService(
serviceName = "OrderService",
portName = "OrderPort",
endpointInterface = "com.example.soap.OrderPortType",
targetNamespace = "http://example.com/order/v1"
)
@BindingType(SOAPBinding.SOAP11HTTP_BINDING)
public class OrderSoapEndpoint implements OrderPortType {
private final OrderApplicationService orderApplicationService;
private final OrderSoapMapper mapper;
public OrderSoapEndpoint(OrderApplicationService orderApplicationService,
OrderSoapMapper mapper) {
this.orderApplicationService = orderApplicationService;
this.mapper = mapper;
}
@Override
public SubmitOrderResponse submitOrder(SubmitOrderRequest request)
throws BusinessFault, TechnicalFault {
try {
// Pattern: Adapter/Mapper - SOAP DTO wird vom Domänenmodell getrennt.
SubmitOrderCommand command = mapper.toCommand(request);
// Pattern: Application Service - Use Case statt Logik im Endpoint.
OrderResult result = orderApplicationService.submitOrder(command);
return mapper.toSoapResponse(result);
} catch (CustomerBlockedException ex) {
throw BusinessFaults.customerBlocked(ex.customerNumber());
} catch (BackendUnavailableException ex) {
throw TechnicalFaults.backendUnavailable("ORDER_BACKEND_DOWN");
}
}
}
Der Endpoint ist bewusst dünn. Fachlogik gehört in OrderApplicationService. Mapping gehört in OrderSoapMapper. Fehlerübersetzung gehört in eine klare Fault-Fabrik. Dadurch kann man später REST, Messaging oder Batch als weitere Adapter hinzufügen, ohne die Fachlogik zu duplizieren.
8. SOAP Faults: Fachfehler und technische Fehler trennen
Ein SOAP Fault ist die standardisierte Fehlerstruktur einer SOAP-Nachricht. Trotzdem muss man im Enterprise sauber unterscheiden: Ein fachlicher Fehler ist kein technischer Ausfall. Ein unbekannter Kunde, ein überschrittenes Kreditlimit oder eine gesperrte Kundennummer sind fachliche Resultate. Ein Timeout, kaputtes Backend oder ungültiges XML ist technisch.
<soapenv:Fault xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>soapenv:Client</faultcode>
<faultstring>Business validation failed</faultstring>
<detail>
<ord:BusinessFault xmlns:ord="http://example.com/order/v1">
<ord:Code>CUSTOMER_BLOCKED</ord:Code>
<ord:Message>Customer is blocked for new orders.</ord:Message>
<ord:CorrelationId>c-2026-07-07-00042</ord:CorrelationId>
</ord:BusinessFault>
</detail>
</soapenv:Fault>
Eine robuste Fehlerstrategie enthält:
- maschinenlesbaren Fehlercode,
- menschenlesbare Kurzbeschreibung,
- Korrelations-ID,
- klare Retry-Entscheidung,
- keine internen Stacktraces im externen Vertrag,
- stabile Fehlercodes über Versionen hinweg.
// Pattern: Factory Method - zentrale Erzeugung konsistenter Faults.
public final class BusinessFaults {
private BusinessFaults() {}
public static BusinessFault customerBlocked(String customerNumber) {
BusinessFaultDetail detail = new BusinessFaultDetail();
detail.setCode("CUSTOMER_BLOCKED");
detail.setMessage("Customer is blocked for new orders.");
detail.setReference(customerNumber);
return new BusinessFault("Business validation failed", detail);
}
}
Ein häufiger Fehler ist, jeden Fehler als HTTP 500 oder generischen SOAP Fault auszugeben. Dann kann der Consumer nicht automatisch entscheiden, ob er korrigieren, erneut senden oder einen Menschen informieren soll.
9. Idempotenz, Retry und doppelte Nachrichten
Enterprise-Netzwerke sind nicht perfekt. Timeouts, Lastspitzen und Backend-Unterbrechungen passieren. Der gefährliche Fall ist nicht nur, dass eine Nachricht fehlschlägt. Gefährlich ist, dass der Client nicht weiß, ob die Bestellung im Backend bereits verarbeitet wurde.
Darum braucht jede verändernde Operation einen fachlich oder technisch eindeutigen Schlüssel. Bei submitOrder kann das die Kombination aus Partner-ID und PartnerOrderId oder ein expliziter IdempotencyKey sein. Der Service speichert diesen Schlüssel mit dem Verarbeitungsergebnis. Kommt dieselbe Nachricht erneut, wird nicht doppelt gebucht, sondern das gespeicherte Ergebnis zurückgegeben oder ein klarer Duplikatstatus geliefert.
CREATE TABLE soap_request_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
partner_id VARCHAR(50) NOT NULL,
idempotency_key VARCHAR(120) NOT NULL,
operation_name VARCHAR(80) NOT NULL,
request_hash VARCHAR(128) NOT NULL,
result_status VARCHAR(40) NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE (partner_id, idempotency_key, operation_name)
);
// Pattern: Repository - technische Idempotenzdaten werden gekapselt.
public class IdempotencyService {
private final SoapRequestLogRepository repository;
public ProcessingDecision decide(String partnerId, String key, String requestHash) {
return repository.find(partnerId, key, "submitOrder")
.map(existing -> existing.sameHash(requestHash)
? ProcessingDecision.returnPreviousResult(existing)
: ProcessingDecision.rejectConflictingDuplicate(existing))
.orElseGet(() -> ProcessingDecision.processNormally(partnerId, key, requestHash));
}
}
Merksatz: Retry ohne Idempotenz ist im Zahlungs-, Bestell- und Buchungskontext gefährlich. Erst Idempotenz designen, dann Retry erlauben.
10. Security: TLS, WS-Security, Signatur und Trust
SOAP-Security wird im Enterprise oft mehrschichtig umgesetzt. TLS schützt den Transportkanal. WS-Security schützt die SOAP-Nachricht selbst, zum Beispiel mit UsernameToken, Timestamp, Signatur oder Verschlüsselung. Der Unterschied ist wichtig: TLS schützt auf dem Weg zwischen zwei Punkten. Eine signierte SOAP-Nachricht kann auch über Zwischenstationen hinweg überprüfbar bleiben.
Typische Bausteine:
- TLS/mTLS für Transportverschlüsselung und Client-Zertifikate.
- WS-Security Timestamp gegen Replay-Angriffe.
- UsernameToken oder X.509-Zertifikat für Identität.
- XML Signature für Integrität und Nichtabstreitbarkeit.
- XML Encryption, wenn einzelne Nachrichtenteile geschützt werden müssen.
- Keystore/Truststore-Prozesse mit Ablaufdatum, Rotation und Notfallplan.
<wsse:Security soapenv:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-1">
<wsu:Created>2026-07-07T10:00:00Z</wsu:Created>
<wsu:Expires>2026-07-07T10:05:00Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken>
<wsse:Username>partner-a</wsse:Username>
<wsse:Password Type="...#PasswordDigest">...</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
Security darf nicht nur im Code versteckt sein. Sie gehört in Betriebsdokumentation, Zertifikatsprozesse, Monitoring und Fehlermeldungen. Ein abgelaufenes Partnerzertifikat ist kein Java-Bug, sondern ein Betriebsprozessproblem.
11. SOAP über ESB, Gateway und Middleware
In großen Organisationen hängt ein SOAP-Service selten direkt am Consumer. Dazwischen liegen häufig API-Gateway, ESB, XML-Firewall, Service Registry, Load Balancer, IAM, MQ-Bridge oder Monitoring-Komponenten. Das ist nicht automatisch schlecht. Es wird aber gefährlich, wenn jede Schicht heimlich Nachrichten transformiert.
Gute Aufgaben für eine Middleware-Schicht:
- Authentifizierung und Autorisierung,
- Schema-Validierung,
- Routing,
- Rate Limiting,
- zentrale Audit-Logs,
- Protokollbrücken, etwa SOAP/HTTP nach JMS,
- technische Transformation zwischen Partnervertrag und internem Vertrag.
Schlechte Aufgaben für Middleware:
- komplexe Fachlogik,
- versteckte Datenbankupdates,
- nicht versionierte Transformationsregeln,
- Fehlercodes, die nicht zum WSDL-Vertrag passen,
- stille Feldänderungen ohne Consumer-Abstimmung.
Partner SOAP Request
-> XML Firewall: Größe, Schema, Security
-> Gateway: Partneridentität, Quota, Correlation-ID
-> ESB: Routing + leichte Transformation
-> Order Service: Fachliche Verarbeitung
-> ESB/Gateway: technische Response-Normalisierung
Bei der Analyse alter Systeme sollte man deshalb nicht nur Java-Code lesen. Man muss auch ESB-Flows, XSLT-Transformationen, Gateway-Policies, Zertifikate und Routingregeln verstehen. Häufig liegt die eigentliche Wahrheit der Schnittstelle nicht vollständig im Repository.
12. Transaktionen: SOAP-Aufruf ist nicht gleich verteilte Transaktion
Ein SOAP-Aufruf über HTTP ist aus Sicht der beteiligten Systeme meist keine saubere Ende-zu-Ende-Transaktion. Der Client sendet eine Nachricht, der Server verarbeitet intern vielleicht mit JTA, Datenbank und JMS. Aber der externe Consumer nimmt normalerweise nicht an derselben lokalen Transaktion teil.
Das bedeutet: Man sollte nicht erwarten, dass ein Timeout automatisch alles zurückrollt. Vielleicht wurde die Bestellung bereits gespeichert, aber die Antwort ging verloren. Genau deshalb sind Idempotenz, Statusabfrage und klare Fehlersemantik wichtig.
Ein robustes Design nutzt häufig diese Kombination:
submitOrdernimmt Auftrag an und liefertOrderIdplus Status.getOrderStatusermöglicht spätere Klärung.- Interne Verarbeitung nutzt lokale Transaktionen.
- Bei langen Prozessen wird asynchron weiterverarbeitet.
- Kompensation ersetzt unrealistische verteilte Rollbacks.
@Transactional
public OrderResult submitOrder(SubmitOrderCommand command) {
Customer customer = customerRepository.findByNumber(command.customerNumber());
creditPolicy.assertOrderAllowed(customer, command.totalAmount());
Order order = Order.create(command);
orderRepository.save(order);
// In derselben lokalen Transaktion wird ein Outbox-Event gespeichert.
outboxRepository.save(OrderSubmittedEvent.from(order));
return OrderResult.accepted(order.id(), "ACCEPTED");
}
Das Outbox-Pattern ist hier oft besser als ein direkter synchroner Aufruf an fünf Backends. Der SOAP-Service bleibt schnell, konsistent und beobachtbar. Nachgelagerte Verarbeitung kann zuverlässig über Events erfolgen.
13. Beispiel 1: SubmitOrder komplett von Request bis Response
Dieses Beispiel zeigt eine erfolgreiche Bestellung. Die SOAP-Nachricht enthält Header für Korrelation und Idempotenz. Der Body enthält die fachliche Bestellung. Die Response liefert eine interne Order-ID und einen Status.
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order/v1">
<soapenv:Header>
<ord:CorrelationId>corr-90001</ord:CorrelationId>
<ord:PartnerId>partner-shop-at</ord:PartnerId>
<ord:IdempotencyKey>partner-shop-at:PO-2026-10001</ord:IdempotencyKey>
</soapenv:Header>
<soapenv:Body>
<ord:SubmitOrderRequest>
<ord:PartnerOrderId>PO-2026-10001</ord:PartnerOrderId>
<ord:CustomerNumber>C-100200</ord:CustomerNumber>
<ord:RequestedDeliveryDate>2026-07-15</ord:RequestedDeliveryDate>
<ord:OrderLine>
<ord:Sku>JAVA-ENTERPRISE-BOOK</ord:Sku>
<ord:Quantity>2</ord:Quantity>
</ord:OrderLine>
<ord:OrderLine>
<ord:Sku>ARCHITECTURE-POSTER</ord:Sku>
<ord:Quantity>1</ord:Quantity>
</ord:OrderLine>
</ord:SubmitOrderRequest>
</soapenv:Body>
</soapenv:Envelope>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ord="http://example.com/order/v1">
<soapenv:Body>
<ord:SubmitOrderResponse>
<ord:OrderId>O-778899</ord:OrderId>
<ord:Status>ACCEPTED</ord:Status>
<ord:ReceivedAt>2026-07-07T10:12:22Z</ord:ReceivedAt>
</ord:SubmitOrderResponse>
</soapenv:Body>
</soapenv:Envelope>
Die technische Qualität erkennt man an drei Dingen: Erstens ist die Nachricht fachlich lesbar. Zweitens enthält sie genug technische Steuerungsdaten für Betrieb und Retry. Drittens gibt es keine internen Implementierungsdetails wie Tabellen, Entity-Namen oder Stacktraces.
14. Beispiel 2: BillingService mit Kreditlimit-Fault
Ein Billing-Service prüft, ob eine Rechnung erzeugt werden darf. Kreditlimit überschritten ist kein technischer Fehler. Der Service läuft korrekt, aber die fachliche Entscheidung ist negativ. Deshalb liefert er einen BusinessFault mit stabilem Code.
<bil:CreateInvoiceRequest xmlns:bil="http://example.com/billing/v1">
<bil:OrderId>O-778899</bil:OrderId>
<bil:CustomerNumber>C-100200</bil:CustomerNumber>
<bil:Currency>EUR</bil:Currency>
<bil:GrossAmount>12900.00</bil:GrossAmount>
</bil:CreateInvoiceRequest>
<soapenv:Fault xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>soapenv:Client</faultcode>
<faultstring>Credit limit exceeded</faultstring>
<detail>
<bil:BillingBusinessFault xmlns:bil="http://example.com/billing/v1">
<bil:Code>CREDIT_LIMIT_EXCEEDED</bil:Code>
<bil:Message>Invoice cannot be created because credit limit is exceeded.</bil:Message>
<bil:RetryAllowed>false</bil:RetryAllowed>
</bil:BillingBusinessFault>
</detail>
</soapenv:Fault>
public InvoiceResponse createInvoice(InvoiceRequest request) throws BillingBusinessFault {
CreditDecision decision = creditService.check(request.customerNumber(), request.grossAmount());
if (!decision.allowed()) {
// Pattern: Domain Policy - fachliche Regel ist separiert und testbar.
throw BillingFaults.creditLimitExceeded(decision.reason());
}
Invoice invoice = invoiceService.createInvoice(request);
return mapper.toResponse(invoice);
}
Dieser Fault ist bewusst fachlich. Ein automatischer Retry wäre falsch. Der Consumer muss entweder Betrag, Kunde oder Freigabeprozess korrigieren.
15. Beispiel 3: SOAP-Client in einer modernen Anwendung
Viele moderne Anwendungen müssen noch alte SOAP-Services konsumieren. Der saubere Weg ist eine Anti-Corruption-Layer: Die neue Anwendung spricht intern ihre eigene Domänensprache. Nur ein Adapter kennt SOAP DTOs, WSDL-Clients, Faults und XML-Details.
// Pattern: Adapter - moderne Domäne bleibt unabhängig vom Legacy-SOAP-Client.
public class LegacyCustomerSoapAdapter implements CustomerRiskPort {
private final LegacyCustomerRiskPortType soapPort;
private final CustomerRiskMapper mapper;
public LegacyCustomerSoapAdapter(LegacyCustomerRiskPortType soapPort,
CustomerRiskMapper mapper) {
this.soapPort = soapPort;
this.mapper = mapper;
}
@Override
public RiskDecision checkRisk(CustomerNumber customerNumber, Money amount) {
try {
LegacyRiskRequest request = mapper.toSoapRequest(customerNumber, amount);
LegacyRiskResponse response = soapPort.checkCustomerRisk(request);
return mapper.toDomainDecision(response);
} catch (LegacyBusinessFault fault) {
return mapper.toDomainDecision(fault);
} catch (WebServiceException ex) {
throw new LegacySystemUnavailableException("Risk SOAP service unavailable", ex);
}
}
}
Wichtig ist, SOAP-Details nicht überall im neuen Code zu verteilen. Sonst wird die moderne Anwendung selbst wieder vom Legacy-Vertrag abhängig. Der Adapter ist die bewusste Grenze.
Neue Anwendung
-> CustomerRiskPort interne Schnittstelle
-> LegacyCustomerSoapAdapter technische SOAP-Integration
-> generierter WSDL-Client fremder Vertrag
-> Legacy-System
16. Beispiel 4: Asynchrone Verarbeitung mit SOAP und JMS/Outbox
Nicht jeder SOAP-Aufruf sollte alles synchron erledigen. Wenn Bestellung, Kreditprüfung, Lagerreservierung, Rechnung und Versand in einem HTTP-Request passieren, entstehen lange Laufzeiten und schwer behandelbare Fehler. Ein besseres Modell ist häufig: SOAP nimmt die Nachricht an, validiert sie, speichert den Auftrag und startet nachgelagerte Verarbeitung asynchron.
@Transactional
public SubmitOrderResponse submitOrder(SubmitOrderRequest request) {
SubmitOrderCommand command = mapper.toCommand(request);
ProcessingDecision decision = idempotencyService.decide(
command.partnerId(), command.idempotencyKey(), command.hash());
if (decision.shouldReturnPreviousResult()) {
return mapper.toSoapResponse(decision.previousResult());
}
Order order = orderApplicationService.accept(command);
// Pattern: Outbox - Ereignis wird atomar mit dem Auftrag gespeichert.
outbox.save(new OutboxMessage(
"OrderSubmitted",
order.id().value(),
json.serialize(OrderSubmittedPayload.from(order))
));
return new SubmitOrderResponse(order.id().value(), "ACCEPTED");
}
Die eigentliche Weiterleitung an JMS, Kafka oder ein anderes System passiert danach durch einen Outbox-Publisher. Dadurch wird die SOAP-Antwort schnell und die interne Verarbeitung zuverlässiger.
SOAP SubmitOrder
-> lokale DB-Transaktion: Order + Outbox Event
-> Response ACCEPTED
-> Outbox Publisher liest Event
-> JMS/Kafka sendet an Billing, Warehouse, Reporting
Die fachliche Konsequenz: Der Vertrag muss Statusabfragen unterstützen. Ein Partner muss später fragen können, ob eine angenommene Bestellung schon fakturiert, abgelehnt oder in Klärung ist.
17. Versionierung: Kompatibel erweitern statt brechen
SOAP-Verträge leben lange. Versionierung muss deshalb bewusst geplant werden. Die wichtigste Regel: Bestehende Consumer dürfen nicht brechen, nur weil ein neuer Consumer zusätzliche Felder benötigt.
Meist kompatibel:
- optionale Elemente hinzufügen,
- neue Operation ergänzen,
- neue Fault-Codes dokumentiert ergänzen,
- Enumeration vorsichtig erweitern, wenn Consumer unbekannte Werte tolerieren.
Meist brechend:
- Pflichtfeld hinzufügen,
- Element umbenennen,
- Namespace ändern,
- Datentyp verschärfen,
- bestehende Operation semantisch anders interpretieren,
- Reihenfolge in
xsd:sequenceunbedacht verändern.
<!-- Kompatible Erweiterung: optionales Feld -->
<xsd:element name="SalesChannel" type="xsd:string" minOccurs="0"/>
<!-- Riskant/brechend: neues Pflichtfeld in bestehendem Request -->
<xsd:element name="MandatoryApprovalCode" type="xsd:string" minOccurs="1"/>
Eine saubere Strategie ist, Namespaces pro Major-Version zu verwenden, zum Beispiel /order/v1 und /order/v2. Minor-Erweiterungen bleiben kompatibel im bestehenden Namespace. Major-Änderungen erhalten eigene Endpoints oder zumindest klar getrennte Verträge.
18. Testing: SOAP ist ohne Contract Tests gefährlich
SOAP-Tests sollten nicht nur prüfen, ob eine Java-Methode funktioniert. Sie müssen den Vertrag schützen. Ein guter Testmix enthält Schema-Validierung, Beispielnachrichten, Contract Tests, Fault Tests, Security Tests und Integrationstests gegen eine realistische Umgebung.
@Test
void submitOrderRequestExampleMustStaySchemaValid() {
Source schema = new StreamSource(Path.of("src/main/resources/wsdl/order-v1.xsd").toFile());
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Validator validator = factory.newSchema(schema).newValidator();
assertDoesNotThrow(() -> validator.validate(
new StreamSource(Path.of("src/test/resources/messages/submit-order-request.xml").toFile())
));
}
Sinnvolle Testfälle:
- gültige Bestellung mit mehreren Positionen,
- unbekannter Kunde,
- Kreditlimit überschritten,
- doppelter Idempotency-Key mit gleicher Nachricht,
- doppelter Idempotency-Key mit anderer Nachricht,
- ungültiges XML,
- fehlende Security,
- abgelaufenes Zertifikat,
- Backend-Timeout.
In alten Projekten fehlen oft genau die negativen Beispiele. Dann kennt niemand mehr den beabsichtigten Fehlervertrag. Beim Modernisieren sollte man zuerst echte Beispielnachrichten und Faults sammeln und daraus Tests bauen.
19. Observability und Betrieb: Korrelations-ID ist Pflicht
SOAP-Systeme sind oft verteilt. Ein einzelner Request kann Gateway, ESB, Service, Datenbank, MQ und Partnerantwort berühren. Ohne Korrelations-ID wird Fehlersuche langsam und teuer.
Jede Nachricht sollte eine CorrelationId haben. Wenn der Partner keine liefert, erzeugt das Gateway oder der Service eine. Diese ID erscheint in SOAP Header, Logs, Metriken, Traces und Fehlermeldungen.
public class SoapCorrelationHandler implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext context) {
boolean outbound = Boolean.TRUE.equals(context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY));
String correlationId = Correlation.currentOrCreate();
if (!outbound) {
Mdc.put("correlationId", correlationId);
}
return true;
}
@Override public boolean handleFault(SOAPMessageContext context) { return true; }
@Override public void close(MessageContext context) { Mdc.clear(); }
@Override public Set<QName> getHeaders() { return Set.of(); }
}
Betriebsmetriken für SOAP:
- Requests pro Operation,
- Latenz pro Operation,
- Fault-Rate nach Fault-Code,
- Schema-Validierungsfehler,
- Security-Fehler,
- Backend-Timeouts,
- Retry- und Duplicate-Rate,
- Zertifikatsablaufzeiten.
Gute SOAP-Architektur ist nicht nur XML-Design. Sie ist auch Betriebsdesign.
20. Modernisierung: SOAP nicht blind ersetzen
Viele Modernisierungsprojekte starten mit dem Satz: „Wir ersetzen SOAP durch REST.“ Das kann richtig sein, ist aber keine Strategie. Zuerst muss geklärt werden, welche Consumer existieren, welche Verträge extern bindend sind, welche SLAs gelten und welche Middleware-Regeln versteckt sind.
Sinnvolle Modernisierungspfade:
- SOAP-Service stabil halten, aber Implementierung modernisieren.
- REST-Fassade vor SOAP bauen, wenn neue Consumer einfacher integriert werden sollen.
- Event Bridge ergänzen, wenn nachgelagerte Systeme entkoppelt werden sollen.
- Contract Tests aufbauen, bevor Implementierung ersetzt wird.
- SOAP-Faults und Statusmodelle in moderne Fehler-/Statusmodelle übersetzen.
- Alte
javax.*-Stacks bewusst von neuenjakarta.*-Stacks trennen.
// Pattern: Facade + Anti-Corruption Layer
@RestController
@RequestMapping("/api/orders")
public class OrderRestFacade {
private final LegacyOrderSoapAdapter legacyOrderSoapAdapter;
@PostMapping
public ResponseEntity<OrderDto> create(@RequestBody CreateOrderDto dto) {
SubmitOrderResult result = legacyOrderSoapAdapter.submit(dto);
return ResponseEntity.accepted().body(OrderDto.from(result));
}
}
Die REST-Fassade ist kein einfacher 1:1-Proxy. Sie kann Consumer-freundliche JSON-Modelle, OAuth, moderne Fehlerobjekte und API-Gateway-Policies anbieten. Intern kann sie weiterhin den stabilen SOAP-Vertrag nutzen, bis das Legacy-System schrittweise ersetzt wird.
21. Typische Anti-Patterns in SOAP-Landschaften
God WSDL: Eine riesige WSDL enthält hunderte Operationen, gemischte Fachdomänen und historisch gewachsene Typen. Lösung: Domänen schneiden, neue Verträge klein halten, alte Verträge nur stabilisieren.
Database Contract: Externe Nachrichten spiegeln Tabellen und Spalten. Lösung: fachliche DTOs und Mapper verwenden.
Generic Fault: Jeder Fehler ist SYSTEM_ERROR. Lösung: stabile Fehlercodes und maschinenlesbare Details.
Hidden ESB Logic: Fachentscheidungen liegen in XSLT oder Gateway-Regeln. Lösung: Logik dokumentieren, testen und in fachliche Services zurückführen.
No Idempotency: Retry erzeugt doppelte Bestellungen oder Rechnungen. Lösung: Idempotency-Key und Request-Log.
Code-First Drift: Refactoring verändert WSDL unbemerkt. Lösung: Contract-First, WSDL-Diff, Contract Tests.
Stacktrace Leakage: Fault enthält interne Klassen, SQL oder Stacktrace. Lösung: externe Fehlerobjekte und sicheres Logging.
One Operation to Rule Them All: Eine Operation processRequest enthält ein generisches XML-Feld für alles. Lösung: fachliche Operationen und typisierte Schemas.
22. Troubleshooting-Leitfaden
Wenn SOAP nicht funktioniert, sollte man systematisch prüfen:
1. Ist der Endpoint erreichbar? DNS, Load Balancer, Firewall, TLS. 2. Passt der SOAPAction-Header oder SOAP 1.2 Action-Parameter? 3. Stimmen Namespace, Elementname und Reihenfolge? 4. Wurde document/literal wrapped erwartet, aber anders gesendet? 5. Ist das XML Schema valide? 6. Sind Zertifikat, Truststore und Uhrzeit korrekt? 7. Gibt es ein Gateway oder ESB, das die Nachricht verändert? 8. Ist der Fault fachlich oder technisch? 9. Gibt es im Log dieselbe Correlation-ID? 10. Wurde eine Nachricht doppelt gesendet?
# Grober Smoke-Test mit curl gegen einen SOAP 1.1 Endpoint
curl -v \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "submitOrder"' \
--data @submit-order-request.xml \
https://api.company.example/services/OrderService
Bei SOAP-Problemen ist der rohe XML-Austausch oft entscheidend. Generierte Clients verstecken Details. Für Fehlersuche braucht man Request, Response, HTTP Header, TLS-Informationen und Gateway-Logs.
23. Kompakte Lern-Checkliste
Nach diesem Lehrbuch solltest du diese Fragen beantworten können:
- Was ist der Unterschied zwischen SOAP Envelope, Header, Body und Fault?
- Welche Rolle spielen WSDL und XSD?
- Warum ist Contract-First in Enterprise-Systemen oft besser als Code-First?
- Was ist
document/literal wrapped? - Wie trennt man fachliche und technische Fehler?
- Warum ist Idempotenz bei Retry entscheidend?
- Welche Security-Schichten gibt es bei SOAP?
- Warum ist ein SOAP-Aufruf nicht automatisch eine verteilte Transaktion?
- Wie testet man SOAP-Verträge?
- Wie modernisiert man SOAP-Landschaften, ohne bestehende Consumer zu brechen?
Praxisübung: Nimm eine reale WSDL aus einem Legacy-System und markiere: Namespace, Types, Messages, PortType, Binding, Service, Faults, Security-Annahmen, mögliche Versionierungsrisiken und versteckte Middleware-Abhängigkeiten.
Quellen und Standards
- W3C: SOAP Version 1.2 Part 1 - Messaging Framework, Second Edition, 27.04.2007, https://www.w3.org/TR/soap12-part1/
- W3C: Web Services Description Language Version 2.0, 26.06.2007, https://www.w3.org/TR/2007/REC-wsdl20-20070626/
- WSDL 1.1 Note, 15.03.2001, https://www.w3.org/TR/wsdl/
- WS-I Basic Profile 1.2, https://ws-i.org/profiles/BasicProfile-1.2-2010-02-16.html
- Jakarta XML Web Services, Eclipse Foundation, https://jakarta.ee/specifications/xml-web-services/
- Jakarta EE Tutorial: Building Web Services with Jakarta XML Web Services, https://jakarta.ee/learn/docs/jakartaee-tutorial/9.1/websvcs/jaxws/jaxws.html
Hinweis: Das Lehrbuch erklärt bewusst praxisorientiert und verwendet vereinfachte Beispielverträge. In echten Projekten müssen WSDL, XSD, Security-Policies, Zertifikate, Laufzeitserver und Unternehmensstandards konkret geprüft werden.