The Problem Statement
If you've ever worked on an e-commerce platform that needs to talk to SAP, you already know the fun: two worlds with completely different ideas about data, timing, and what constitutes an "error." On one side, you've got a modern web application that needs sub-second responses during checkout. On the other, you've got an ERP system designed for batch processing and back-office workflows that communicates over SOAP.
This post walks through the architecture we built to bridge that gap β covering the data flow from storefront to SAP, how we handle failures (spoiler: there are many flavors), and design decisions we'd either repeat or do differently next time.
Architecture Overview: The Three-Layer Sandwich
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Web Layer (Controllers / API Endpoints) β
β Handles HTTP requests, auth, routing β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer (Domain Services) β
β Orchestrates business logic, maps models β
ββββββββββββββββββββββββββββββββββββββββββββββββ€
β ERP Integration Layer (SOAP Clients) β
β Translates domain objects β SOAP calls β
β Manages credentials, timeouts, retries β
ββββββββββββββββββββββββββββββββββββββββββββββββ
The key insight: the integration layer is its own isolated project. When SAP changes its WSDL (and it will), the blast radius is contained to a single project.
Data Flow: From Cart to SAP
Two-Phase Order Handoff
Phase 1 β Order Simulation (during checkout)
[Customer Cart] β [YourOrderService].SimulateOrder()
β Convert address DTO β [ErpAddress]
β Convert line items β [ErpLineItem[]]
β Load locale-specific ERP config
β Call ERP SOAP endpoint: SimulateOrder
β Parse response β extract tax + shipping totals
β Return to checkout UI
Phase 2 β Order Creation (post-payment)
[OrderPlaced Event] β [YourOrderHandler].Handle()
β Load ERP config for current locale
β Check if real-time ordering is enabled
β Build shipping + billing addresses
β Call ERP SOAP endpoint: CreateOrder
β On success: store ERP order number
β On failure: set order status, notify admins
The Event-Driven Decoupling
// Illustrative β not actual production code
public class [YourOrderHandler] : INotificationHandler<[OrderPlacedEvent]>
{
public async Task Handle([OrderPlacedEvent] notification, CancellationToken ct)
{
var erpConfig = _configProvider.GetErpConfig(currentLocale);
if (erpConfig == null || !erpConfig.RealTimeOrderEnabled)
{
await _mediator.Publish(new [StaffNotification](order), ct);
return;
}
var errors = _erpOrderService.CreateOrder(
shippingAddress, billingAddress, lineItems,
paymentInfo, erpConfig, out string erpOrderNumber);
if (errors.Any())
await HandleErpFailure(order, errors, ct);
else
await HandleErpSuccess(order, erpOrderNumber, ct);
}
}
The SOAP Client: Configuration Matters More Than You Think
private [ErpSoapClient] BuildSoapClient()
{
var binding = new BasicHttpBinding
{
Security = {
Mode = BasicHttpSecurityMode.Transport,
Transport = { ClientCredentialType = HttpClientCredentialType.Basic }
},
MaxReceivedMessageSize = 20_000_000,
SendTimeout = TimeSpan.FromSeconds(90)
};
var client = new [ErpSoapClient](binding, new EndpointAddress(_erpEndpoint));
client.ClientCredentials.UserName.UserName = _erpUsername;
client.ClientCredentials.UserName.Password = _erpPassword;
return client;
}
- 90-second timeout. SAP is not fast. Default WCF timeouts will kill you.
- 20MB buffer sizes. B2B order history responses can be massive.
- No client reuse. WCF channel faulting breaks all subsequent calls on a faulted instance.
Error Handling: The Taxonomy of SAP Failures
1. Pre-flight Validation
if (config.SalesOrganization.IsNullOrEmpty())
{
errors.Add(new [ErpMessage]("E", "", "ERP config missing for this locale"));
return errors;
}
2. SOAP Response Errors
var fatalErrors = erpResponse.Messages
.Where(m => m.Type == "E" || m.Type == "Error")
.Where(m => !(m.Number == "219" && m.Category == "V4")) // Known non-fatal
.Select(m => new [ErpMessage](m.Type, m.Number, m.Text));
3. Null Response & 4. Exception-Level Failures
Every layer of the response must be null-checked defensively. Network timeouts and SOAP faults are caught at the top level and trigger the admin notification pipeline.
Admin Notification Pipeline
Order sync fails
β
βββ Set order status β AwaitingExchange
βββ Attach error note to order
βββ Send templated email to ERP admins
βββ Fallback: current locale β parent locale β English
Customer Sync: The Other Half
E-commerce β SAP: New customer creation, address verification, VAT/PAN checks
SAP β E-commerce: Address sync (SAP is master), customer number assignment
SAP β Identity: SAP customer number pushed back to identity provider
Lessons Learned
- SAP config is per-locale, not global. Model this from day one.
- "Simulate before you create" saves orders. Catches pricing issues before payment.
- Not all SAP "errors" are errors. Build a catalog of known non-fatal messages.
- SOAP isn't dead, it's just hiding. Design the transport as a swappable layer.
- Payment mapping is complex. Varies by processor and market.
- Never trust negative values. Validate SAP tax/shipping totals before checkout.
Final Thoughts
Building an e-commerce-to-SAP integration is fundamentally a translation problem. Isolate the translation layer, validate aggressively at every boundary, and assume every SAP call can fail in ways you haven't imagined yet.
This post describes architectural patterns observed in production e-commerce SAP integrations. All identifiers, endpoints, and implementation details have been genericized.