If you work with integrations for long enough, sooner or later you will run into SOAP. It is not an archaeological relic: banks, insurers, public administrations and healthcare systems still expose thousands of SOAP services in production, and they are not going away any time soon. Understanding what it is, why it was designed that way and how it differs from REST will serve you in two very concrete ways: integrating with those systems when the time comes, and better understanding why REST made the decisions it did. In this lesson we will see the same Aroma Store query — fetching a coffee's data — solved in both styles, side by side.

Contents

  1. What SOAP is exactly
  2. The structure of the envelope: Envelope, Header and Body
  3. The contract: WSDL
  4. The WS-* stack
  5. The same case side by side: looking up a coffee
  6. Error handling: SOAP Fault versus HTTP codes
  7. Full comparison table
  8. When SOAP still makes sense
  9. When to choose REST
  10. REST façades over legacy SOAP services

  1. What SOAP is exactly

SOAP started life as an acronym for Simple Object Access Protocol, although from version 1.2 onwards the W3C stopped expanding the initials (among other reasons, because there was little that was simple about it). Unlike REST, which is an architectural style, SOAP is a protocol: it has a formal specification that defines exactly what a valid message must look like.

Its defining traits:

  • XML-based: every message is an XML document with a fixed structure.
  • Transport-independent: it can travel over HTTP, but also over SMTP (email), JMS (message queues) or raw TCP. This neutrality was an explicit design goal.
  • Operation-oriented, not resource-oriented: you invoke methods such as getCoffee or createOrder, RPC style.
  • A formal contract described in WSDL, machine-readable, from which clients are generated automatically.
  • Extensible through the WS-* stack for security, reliability and transactions.

When SOAP travels over HTTP — the most common case — it always uses POST against a single endpoint. You will recognise the pattern: it is exactly level 0 of the Richardson model we saw in the previous lesson. HTTP acts as a mere transport tunnel.

  1. The structure of the envelope: Envelope, Header and Body

Every SOAP message is an "envelope" with the same anatomy:

graph TD
    E["<b>Envelope</b><br/>the envelope; mandatory root"] --> H["<b>Header</b><br/>optional: security, transactions,<br/>routing, correlation"]
    E --> B["<b>Body</b><br/>mandatory: the call<br/>or its result"]
    B --> F["<b>Fault</b><br/>inside Body,<br/>only when there is an error"]
  • Envelope: the root element. It identifies the document as a SOAP message.
  • Header: optional, containing infrastructure metadata. This is where WS-Security (signatures, credentials), transaction identifiers and routing live. It is the conceptual equivalent of HTTP headers, but inside the message, which lets them survive any change of transport.
  • Body: mandatory, containing the payload: the operation being invoked with its parameters, or the result.
  • Fault: a special element inside Body that represents an error.

This separation between Header and Body is more elegant than it looks: it lets an intermediary process the security in the Header without touching the business content, and it lets a signed message remain verifiable even if it changes transport three times along the way.

  1. The contract: WSDL

WSDL (Web Services Description Language) is an XML document that formally describes the service: which operations it offers, which data types it uses, which messages are exchanged and at which address it is available.

Its practical value is enormous and worth acknowledging: from a WSDL, tools automatically generate the complete client code in Java, C# or whatever language you like. The developer writes service.getCoffee("cof_001") and never sees a single byte of XML.

A simplified fragment of the Aroma Store WSDL:

<definitions name="CoffeeService"
             targetNamespace="http://aromastore.example/services"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <!-- 1. Types: the exact structure of the data, validatable -->
  <types>
    <xsd:schema targetNamespace="http://aromastore.example/services">
      <xsd:element name="GetCoffeeRequest">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="coffeeId" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="GetCoffeeResponse">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="id"          type="xsd:string"/>
            <xsd:element name="name"        type="xsd:string"/>
            <xsd:element name="origin"      type="xsd:string"/>
            <xsd:element name="roast"       type="xsd:string"/>
            <xsd:element name="priceEuros"  type="xsd:decimal"/>
            <xsd:element name="stock"       type="xsd:int"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </types>

  <!-- 2. Available operations -->
  <portType name="CoffeesPortType">
    <operation name="getCoffee">
      <input  message="tns:GetCoffeeRequest"/>
      <output message="tns:GetCoffeeResponse"/>
    </operation>
  </portType>

  <!-- 3. Where the service lives -->
  <service name="CoffeeService">
    <port name="CoffeesPort" binding="tns:CoffeesBinding">
      <soap:address location="https://services.aromastore.example/coffees"/>
    </port>
  </service>
</definitions>

Note the real strength of this approach: priceEuros is declared as xsd:decimal and stock as xsd:int. A message sending "fourteen fifty" is invalid and is rejected automatically, without writing a line of validation. In REST, that role is played today by OpenAPI and JSON Schema, but optionally (lessons 03-04 and 05-02).

  1. The WS-* stack

A family of specifications was built on top of SOAP to cover enterprise needs that HTTP did not solve on its own:

Specification What it provides Rough equivalent in the REST world
WS-Security Signing and encryption at the message level, credentials in the Header TLS (at the channel level) + signed JWTs
WS-ReliableMessaging Delivery and ordering guarantees, with retries and acknowledgements Retries with idempotency; message queues
WS-AtomicTransaction Distributed transactions across several services (commit all or nothing) No direct equivalent: saga patterns, compensations
WS-Addressing Transport-independent addressing and correlation HTTP and correlation headers
WS-Policy Declaration of requirements (what encryption the service demands) Documentation and gateway configuration

There are two capabilities here that REST genuinely does not match, and it is worth acknowledging that without embarrassment:

  1. Message-level security. TLS encrypts the channel: at each intermediate hop the message is decrypted. WS-Security signs and encrypts the content, so it can pass through five intermediaries and still be verifiable at its destination. For a bank transfer instruction with legal standing, the difference is not theoretical.
  2. Distributed transactions. WS-AtomicTransaction lets you coordinate an operation spanning several services with two-phase commit. In the REST world this is solved with compensation patterns, which are simpler to operate but offer weaker guarantees.

The price of all this was complexity: the specifications numbered in the dozens, not all implementations were compatible with each other, and developing without an IDE to generate the code was very costly.

  1. The same case side by side: looking up a coffee

Nothing clarifies things like seeing the same operation in both styles. We want to fetch the data for the coffee cof_001 from Aroma Store.

In SOAP

The request:

POST /services/coffees HTTP/1.1
Host: services.aromastore.example
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://aromastore.example/services/getCoffee"
Content-Length: 412

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
               xmlns:aro="http://aromastore.example/services">
  <soap:Header>
    <aro:Credentials>
      <aro:user>internal_panel</aro:user>
      <aro:token>eyJhbGciOiJIUzI1NiJ9...</aro:token>
    </aro:Credentials>
  </soap:Header>
  <soap:Body>
    <aro:GetCoffeeRequest>
      <aro:coffeeId>cof_001</aro:coffeeId>
    </aro:GetCoffeeRequest>
  </soap:Body>
</soap:Envelope>

The response:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: 498

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
               xmlns:aro="http://aromastore.example/services">
  <soap:Body>
    <aro:GetCoffeeResponse>
      <aro:id>cof_001</aro:id>
      <aro:name>Ethiopia Yirgacheffe</aro:name>
      <aro:origin>Ethiopia</aro:origin>
      <aro:roast>light</aro:roast>
      <aro:priceEuros>14.50</aro:priceEuros>
      <aro:stock>120</aro:stock>
    </aro:GetCoffeeResponse>
  </soap:Body>
</soap:Envelope>

Things worth pointing out:

  • POST is used even though the operation only reads data. Consequence: no intermediate cache can reuse this response.
  • The endpoint is a single one (/services/coffees); the specific coffee travels in the body. There is no URL identifying cof_001, so it cannot be linked or bookmarked.
  • The SOAPAction header states which operation is being invoked. It is a SOAP-specific mechanism, foreign to HTTP.
  • Namespaces (xmlns) avoid collisions between vocabularies, at the cost of a lot of visual noise.
  • The response is 200 OK even when there is a business error: the real result is inside the body.

In REST

The request:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
     -H "Accept: application/json" \
     https://api.aromastore.example/v1/coffees/cof_001

Raw:

GET /v1/coffees/cof_001 HTTP/1.1
Host: api.aromastore.example
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

The response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300
ETag: "a7f3c9"

{
  "id": "cof_001",
  "name": "Ethiopia Yirgacheffe",
  "origin": "Ethiopia",
  "roast": "light",
  "priceEuros": 14.50,
  "stock": 120
}

The size comparison is devastating: around 900 bytes there and back in SOAP against around 200 in REST, for exactly the same information. In a mobile app that queries the catalogue hundreds of times, that translates into data, battery and time.

But saving bytes is not the most important part. What is decisive is that in REST:

  • The coffee has its own URL that can be shared, linked and tried out from the browser.
  • The operation is a GET, so it is cacheable (max-age=300) and safe to retry.
  • The result is communicated with the protocol's own status code.
  • Anyone can try it with curl in ten seconds, without generating code or installing anything.

  1. Error handling: SOAP Fault versus HTTP codes

When the coffee does not exist, SOAP responds with a Fault:

HTTP/1.1 500 Internal Server Error
Content-Type: text/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
  <soap:Body>
    <soap:Fault>
      <soap:Code>
        <soap:Value>soap:Sender</soap:Value>
      </soap:Code>
      <soap:Reason>
        <soap:Text xml:lang="en">The requested coffee does not exist</soap:Text>
      </soap:Reason>
      <soap:Detail>
        <aro:ErrorCode>COFFEE_NOT_FOUND</aro:ErrorCode>
        <aro:CoffeeId>cof_999</aro:CoffeeId>
      </soap:Detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

Note the inconsistency the model drags along: the error belongs to the client (it asked for a coffee that does not exist), yet HTTP returns 500, which means "server error". SOAP 1.1 forced this; SOAP 1.2 relaxed it, but the pattern is still alive in many services. The result is that monitoring based on HTTP codes is useless: you have to open the XML to find out what happened.

In REST, the same error:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "code": "coffee_not_found",
    "message": "There is no coffee with the identifier cof_999"
  }
}

The status code already says everything machines need, and the body supplies the detail for people. A monitoring dashboard tells at a glance between "clients are asking for things that do not exist" (4xx) and "my service is broken" (5xx). We will look at detailed error design in 02-04 and 03-07.

Aspect SOAP Fault HTTP codes
Where the error lives In the XML body In the status line
Distinguishes fault Sender / Receiver inside the XML 4xx / 5xx, visible without parsing
Visible to intermediaries No Yes
Standardised Yes, fixed structure Yes, codes; the body is free-form (or RFC 9457)
Business detail In Detail In the JSON body

  1. Full comparison table

Criterion SOAP REST
Nature A protocol with a formal specification An architectural style
Format XML only Any; in practice JSON
Contract WSDL, mandatory and formal OpenAPI, optional
Transport Agnostic: HTTP, SMTP, JMS, TCP HTTP exclusively
Verbs POST only (when it runs over HTTP) GET, POST, PUT, PATCH, DELETE
Addressing One endpoint per service One URI per resource
State Can be stateful or stateless Stateless by definition
Caching Does not use HTTP's Native to the protocol
Security WS-Security (message level) + TLS TLS + OAuth 2.0 / JWT (channel level)
Transactions WS-AtomicTransaction Not standardised; sagas and compensation
Errors SOAP Fault in the body Status codes + body
Message size Large (envelope, namespaces, XML) Small
Performance Lower: XML parsing and verbosity Higher
Learning curve Steep Gentle
Tooling Excellent in Java and .NET; scarce elsewhere Universal; a browser or curl is enough
Client generation Automatic from WSDL Automatic from OpenAPI, if it exists
Consumption from a browser Very awkward Natural
Current adoption Legacy systems and regulated sectors The de facto standard on the web

  1. When SOAP still makes sense

It would be a mistake to caricature SOAP as "the old, bad thing". There are contexts where its properties are still the right ones:

  • Banking and interbank payment systems. Many financial protocols require a digital signature of the message with evidential value, and the sector's standard is often already defined in SOAP.
  • Insurance. Exchanges between insurers and with regulatory bodies are standardised in XML schemas consolidated twenty years ago.
  • Healthcare. HL7 and other clinical standards have a long XML tradition, with strict structure and validation requirements.
  • Public administration. Many electronic invoicing, notification and signature services are defined as SOAP services.
  • Legacy systems. An ERP fifteen years old exposes SOAP, and rewriting it is not a realistic option.
  • Distributed transaction requirements. When you genuinely need "all or nothing" across several services with strong guarantees.
  • Contracts that must be binding and verifiable between organisations, with strict validation and non-repudiation.

The common denominator: regulated environments, with contracts between organisations, legal signature requirements and long-lived systems.

  1. When to choose REST

For everything else, and certainly for a new web-oriented project:

  • Public APIs you want people to adopt without friction.
  • Web and mobile applications, where message weight and client simplicity matter.
  • Cacheable content, such as the Aroma Store catalogue.
  • Third-party ecosystems: the easier it is to get started, the more integrations you will have.
  • Heterogeneous teams with different languages and no uniform enterprise tooling.
  • Fast iteration, where generating and regenerating formal contracts would slow development down.

For Aroma Store the decision is obvious: its API is consumed by a website, a mobile app, an internal panel and external partners. There is no digital signature with legal force, no distributed transactions between organisations, and there is a clear need for caching and easy adoption. REST, without hesitation.

  1. REST façades over legacy SOAP services

A pattern you will come across extremely often in medium and large companies: the legacy SOAP system cannot be thrown away, but modern clients (mobile, web, partners) do not want to touch XML. The solution is a REST façade that translates.

graph LR
    A["Aroma Mobile"] -->|"GET /v1/invoices/inv_88<br/>JSON"| F["REST façade<br/>(Node.js / gateway)"]
    W["Website"] -->|JSON| F
    F -->|"SOAP + XML<br/>getInvoice"| L["Legacy invoicing<br/>system (SOAP)"]
    F -->|"SOAP + XML"| C["Accounting ERP<br/>(SOAP)"]

Suppose Aroma Store's invoicing is handled by an old ERP that only speaks SOAP. The façade:

  1. Receives GET /v1/orders/ord_5001/invoice with a modern token.
  2. Validates permissions and translates the request into a SOAP envelope with the credentials the ERP expects.
  3. Receives the XML, extracts the data and turns it into clean JSON with names consistent with the rest of the API.
  4. Translates Faults into appropriate HTTP status codes (404, 403, 503).
  5. Adds caching where it makes sense, relieving load on an old and fragile system.

Advantages: new clients see a homogeneous API, the legacy system is untouched and it can be replaced behind the scenes without anyone noticing. Drawbacks: one more hop of latency, one more component to maintain and the risk that the façade ends up leaking odd concepts from the old system (cryptic codes, fields with incomprehensible names) if the design is not looked after.

This work of translating and homogenising is exactly one of the functions of an API gateway, which we will see in lesson 05-06.

Common Mistakes and Tips

  • Saying "a RESTful SOAP API". They are things of different categories: SOAP is a protocol, REST a style. A SOAP service is, by construction, at Richardson level 0.
  • Dismissing SOAP as old. If you have to integrate with banking or public administration, you will find solid solutions that have been working for decades. The professional attitude is to understand why they are the way they are.
  • Believing that TLS is equivalent to WS-Security. TLS protects the channel point to point; WS-Security protects the message end to end, across intermediaries. They are different guarantees.
  • Reproducing SOAP with JSON. A single endpoint receiving {"operation": "..."} is SOAP without its advantages: you get the rigidity of level 0 and none of its formal guarantees.
  • Forgetting the correct Content-Type when calling a SOAP service. SOAP 1.1 expects text/xml; SOAP 1.2, application/soap+xml. Mixing them up produces baffling errors.
  • Designing a REST façade as a carbon copy of the SOAP service. If your API exposes POST /v1/getInvoiceRequest, you have relocated the problem instead of solving it.
  • Tip: if you work with a SOAP service, always ask for the WSDL first. With it, tools such as SoapUI or your language's generators give you a working client in minutes.

Exercises

Exercise 1: analyse a SOAP message

Given this message, answer: (a) which operation does it invoke?; (b) where do the credentials travel and why there rather than in an HTTP header?; (c) could the response be cached?; (d) what would the REST equivalent of this operation be?

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
               xmlns:aro="http://aromastore.example/services">
  <soap:Header>
    <aro:Credentials><aro:token>abc123</aro:token></aro:Credentials>
  </soap:Header>
  <soap:Body>
    <aro:ListCustomerOrdersRequest>
      <aro:customerId>cus_842</aro:customerId>
      <aro:from>2026-01-01</aro:from>
    </aro:ListCustomerOrdersRequest>
  </soap:Body>
</soap:Envelope>

Exercise 2: choose a technology with judgement

For each scenario, decide SOAP or REST and justify it with two concrete arguments:

  1. Aroma Store wants coffee blogs to display its catalogue.
  2. A bank must send digitally signed transfer instructions to another bank, with non-repudiation and legal validity.
  3. Aroma Mobile needs to load the catalogue quickly on slow mobile networks.
  4. An insurer must exchange claim reports with a consortium that has already defined a standard XML schema.
  5. An internal inventory service must update stock in three systems and guarantee that, if one fails, none is left modified.

Exercise 3: design a REST façade

Aroma Store's legacy ERP exposes these three SOAP operations. Design the equivalent REST façade, stating the method, the path, the status code on success and which HTTP error you would return in the case indicated.

SOAP operation Description Error case
getInvoiceByOrder(orderId) Returns an order's invoice The order has no invoice yet
cancelInvoice(invoiceId, reason) Cancels an issued invoice The invoice is already cancelled
listCustomerInvoices(customerId, year) A customer's invoices in a given year The customer does not exist

Solutions

Solution 1

  • (a) It invokes ListCustomerOrders, asking for the orders of customer cus_842 since 1 January 2026.
  • (b) The credentials travel in the SOAP envelope's Header. The design reason is transport independence: if the message travels over SMTP or a JMS queue instead of HTTP, there are no HTTP headers to put them in. In addition, this way they can be signed along with the message and survive intermediaries.
  • (c) No. It is a POST to a single endpoint, and neither HTTP caches nor proxies can know that it is really a read. The protocol's caching mechanism is lost entirely.
  • (d) GET /v1/orders?customerId=cus_842&dateFrom=2026-01-01, with Authorization: Bearer abc123 and a 200 OK response. An equally valid alternative: GET /v1/customers/cus_842/orders?dateFrom=2026-01-01, which expresses the relationship in the path.

Solution 2

  1. REST. Adoption by third parties must be immediate (curl or fetch is enough), and the catalogue is cacheable content, which reduces load and latency.
  2. SOAP. Message-level signing with non-repudiation is needed (WS-Security), which TLS does not provide, and the sector's interbank standard is most likely already defined in SOAP.
  3. REST. JSON messages weigh a fraction of a SOAP envelope, and catalogue responses can be cached on the device and in a CDN.
  4. SOAP. The XML schema already exists and is binding between the parties; the WSDL generates validated clients automatically and strict validation is a requirement, not a convenience.
  5. It depends, and this is the most nuanced case. If strict atomicity across heterogeneous systems is required, WS-AtomicTransaction offers it in a standard way. In a modern architecture, however, the usual approach is to solve it with REST or messaging plus a saga pattern with compensating operations, accepting eventual consistency in exchange for far less operational complexity. What matters is that the decision is explicit.

Solution 3

GET    /v1/orders/ord_5001/invoice           -> 200 OK   | error: 404 Not Found
POST   /v1/invoices/inv_88/cancellation      -> 201 Created (or 200 OK) | error: 409 Conflict
GET    /v1/customers/cus_842/invoices?year=2026 -> 200 OK | error: 404 Not Found

Justification:

  • Get invoice: it is a read, so GET. The invoice is modelled as a sub-resource of the order, which reflects the relationship. If it does not exist yet, 404 Not Found, because the requested resource is not there.
  • Cancel invoice: DELETE is not used, because cancelling is not deleting: the invoice still exists with a cancelled status, and in accounting that is mandatory. The cancellation is modelled as a sub-resource that you POST to, sending the reason in the body. If it was already cancelled, 409 Conflict, which expresses exactly "the resource's current state prevents this operation". (A defensible alternative is PATCH /v1/invoices/inv_88 with {"status":"cancelled"}; the sub-resource is more expressive when the action requires data of its own, such as the reason.)
  • List a customer's invoices: a GET on the sub-collection, with the year as a filter in the query string, since it modulates the query rather than identifying the resource. If the customer does not exist, 404; if they exist but have no invoices that year, 200 OK with an empty list, not 404: the collection exists, it is simply empty.

Conclusion

SOAP and REST answer two different philosophies: SOAP is a formal, transport-agnostic protocol, with a WSDL contract and a stack of extensions for security, reliability and transactions; REST is a style that builds on HTTP and makes use of its resources, verbs, codes and caching. We have seen the same coffee lookup in both, confirming that SOAP multiplies the message size by four, gives up caching and hides the result inside the body, while REST turns the coffee into an addressable, cacheable resource you can try from a browser. We have also acknowledged what SOAP does better — end-to-end message signing, strict validation and distributed transactions — and why it is still alive in banking, insurance, healthcare and public administration, along with the usual pattern of wrapping those services in a REST façade.

One last piece of the map remains. In the next lesson, REST Compared with GraphQL, gRPC and Webhooks, we will look at the contemporary alternatives: which specific REST problems each one solves, which new problems they introduce and why the norm today is not to pick just one, but to combine them. With that we will close the module and be ready to design the Aroma Store API in module 2.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved