Why Contract-First Design Is Becoming Relevant Again — and SOAP Already Solved It.

Choosing between REST, gRPC, GraphQL or SOAP isn’t a personality test. It’s an architectural decision driven by constraints.
REST dominates public APIs.
gRPC thrives in high-throughput microservices.
GraphQL shines in frontend-heavy systems.
SOAP was supposed to be dead by now. Yet in finance, healthcare and government, it quietly runs some of the most critical integrations in the world — not because teams prefer XML, but because regulators demand the kind of formal, enforceable contracts that SOAP was built to deliver.
⚡ TL;DR (Quick Recap)
- SOAP remains standard in regulated domains due to strict contracts and WS-Security.
- Spring Boot + Spring-WS makes SOAP development clean and testable.
- The production flow is: XSD → generate classes → wire beans → implement endpoint → map faults → test.
- SOAP wins on governance and message-level security — not raw throughput.
The 2026 Stack
We’re operating in:
- Java 21+
- Jakarta EE 11
- Spring Framework 7
- Spring Boot 4
The core dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<!-- WSDL Support -->
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>
For schema-driven code generation:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>4.1.0</version>
<executions>
<execution>
<goals><goal>xjc</goal></goals>
</execution>
</executions>
<configuration>
<sources>src/main/resources/schema</sources>
<outputDirectory>target/generated-sources/jaxb</outputDirectory>
<packageName>io.github.mm.soap.gen</packageName>
</configuration>
</plugin>
Change the XSD. Regenerate. Never edit generated classes manually.
Contract-First: Start With the Schema
SOAP development begins with the contract.
Minimal example:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://github.io/mm/soap/demo"
elementFormDefault="qualified">
<xs:element name="GetDemoRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="GetDemoResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:string"/>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Running mvn generate-sources produces typed Java classes.
This is SOAP’s core strength: strict, versioned contracts enforced at compile time and runtime.
Wiring Spring-WS
Spring-WS relies on four infrastructure beans.
@Configuration
@EnableWs
public class WebServiceConfig {
@Bean
public ServletRegistrationBean<MessageDispatcherServlet>
messageDispatcherServlet(ApplicationContext context) {
var servlet =
new MessageDispatcherServlet();
servlet.setApplicationContext(context);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/ws/*");
}
@Bean("demo-service")
public DefaultWsdl11Definition wsdl(XsdSchema schema) {
var def =
new DefaultWsdl11Definition();
def.setSchema(schema);
def.setLocationUri("/ws");
def.setPortTypeName("DemoServicePortType");
def.setTargetNamespace(
"http://github.io/mm/soap/demo");
return def;
}
@Bean
public XsdSchema demoSchema() {
return new SimpleXsdSchema(
new ClassPathResource("schema/demo.xsd"));
}
@Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller m = new Jaxb2Marshaller();
m.setContextPath("io.github.mm.soap.gen");
return m;
}
}
Data flow:
HTTP → MessageDispatcherServlet → Endpoint → Service → JAXB → SOAP Envelope → Response
No manual XML handling required.
Implementing the Endpoint
@Endpoint
public class SoapDemoEndpoint {
private static final String NAMESPACE =
"http://github.io/mm/soap/demo";
private final DemoService service;
public SoapDemoEndpoint(DemoService service) {
this.service = service;
}
@PayloadRoot(namespace = NAMESPACE,
localPart = "GetDemoRequest")
@ResponsePayload
public GetDemoResponse getDemo(
@RequestPayload GetDemoRequest request) {
var demo = service.getDemo(request.getId());
var response = new GetDemoResponse();
response.setId(demo.id());
response.setName(demo.name());
return response;
}
}
Domain layer with Java 21 records:
public record Demo(String id, String name) {}
@Service
public class DemoService {
// .....
public Demo getDemo(String id) {
return Optional.ofNullable(store.get(id))
.orElseThrow(() ->
new NotFoundException(
"Demo not found: " + id));
}
}Business logic stays clean and framework-independent.
Production Error Handling: SOAP Faults
SOAP communicates errors via structured <soap:Fault> elements.
Example:
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Demo not found</faultstring>
</soap:Fault>
Map exceptions centrally:
@Configuration
public class SoapExceptionHandler
extends SoapFaultMappingExceptionResolver {
public SoapExceptionHandler() {
var mappings = new Properties();
mappings.setProperty(IllegalArgumentException.class.getName(),"CLIENT");
mappings.setProperty(NotFoundException.class.getName(),"SERVER");
var defaultFault = new SoapFaultDefinition();
defaultFault.setFaultCode(SoapFaultDefinition.SERVER);
defaultFault.setFaultStringOrReason("Unexpected server error");
setExceptionMappings(mappings);
setDefaultFault(defaultFault);
setOrder(Integer.MAX_VALUE - 1);
}
}
Guideline:
- CLIENT → validation problems
- SERVER → system failures
Never expose stack traces in production faults.
Testing the Endpoint
@SpringBootTest(
webEnvironment =
SpringBootTest.WebEnvironment.DEFINED_PORT)
class SoapDemoEndpointTest {
@Autowired
private WebServiceTemplate template;
@Test
void getDemo_success() {
var request =
new GetDemoRequest();
request.setId("demo-1");
var response =
(GetDemoResponse)
template.marshalSendAndReceive(
"http://localhost:8088/ws/demo-service",
request);
assertThat(response.getName())
.isEqualTo("Test Demo");
}
@Test
void getDemo_notFound_returnsFault() {
var request =
new GetDemoRequest();
request.setId("invalid");
assertThrows(
SoapFaultClientException.class,
() -> template.marshalSendAndReceive(
"http://localhost:8088/ws/demo-service",
request)
);
}
}
A production SOAP service must be predictable under failure.
Where SOAP Fits in 2026
Let’s be honest.
REST
- Best for public APIs
- Developer-friendly
- Flexible, loosely enforced contracts
gRPC
- High performance
- Strong contracts via Protobuf
- Ideal for internal microservices
GraphQL
- Client-driven data fetching
- Powerful, but complex
SOAP
- Strict WSDL + XSD contracts
- Message-level encryption (WS-Security)
- High interoperability across enterprises
- Strong governance
SOAP does not compete on simplicity. It competes on precision.
Final Takeaways
SOAP in 2026 is not about nostalgia. It’s about specialization.
Spring Boot and modern Java remove much of the friction that once made SOAP painful. With contract-first development, structured fault handling and proper testing, you can build services that meet strict regulatory and interoperability demands.
The right protocol depends on your constraints — not trends. And sometimes, the “old” solution is still the most disciplined one.
You can find example of code on GitHub.
Originally posted on marconak-matej.medium.com.