An SAP landscape is a set of systems that must agree: the ERP, the CRM, the warehouse, the shop floor, the bank, customers and suppliers. Each connection between two of them is an interface. There are only a handful of ways to build one, called integration styles:
Ask and wait (synchronous): one system calls another and waits for the answer. SAP's OData and SOAP APIs and its older RFC calls work this way.
Send and move on (asynchronous): one system hands a message to a queue and carries on. IDocs, EDI and asynchronous SOAP work this way.
Announce (events): a system says "this order changed" and anyone who subscribed is told.
Batch: a file or a bulk extract, once a day or once an hour.
None is best. Each fits some situations and fails in its own way. Ask-and-wait fails when the other side is slow or down. Send-and-move-on can deliver a message twice or late. Events can arrive out of order. Files arrive late and all at once.
An integration lead makes the same decision for every interface: who is waiting, how fast, how many, and what happens when it breaks. This topic gives you that decision.
Interfaces are where processes leak money quietly. A few examples from Calder Pumpworks, the fictional pump maker used throughout this course:
A distributor's order sent twice becomes two sales orders, two shipments and a credit note.
A supplier's late confirmation that never reaches planning means pumps promised on a date the factory can't meet. The exception trace followed exactly this case.
The CRM shows the account manager an old delivery date because a status update overtook a newer one.
A bank statement file that fails overnight leaves cash application and the cash forecast a day behind.
Each of those is a design choice, not bad luck. Choosing the style per interface, and designing its failure handling, decides how many hypercare weeks a program spends reprocessing messages by hand.
There is a second cost: upgrades. An interface built on an interface SAP has released and documented keeps working across upgrades; SAP describes its released APIs as "upgrade-stable" (SAP Learning). An interface built on internal tables or undocumented calls must be retested, and sometimes rebuilt, at each upgrade.
SAP systems offer all the styles, and SAP's platform carries them between systems. As of October 2026:
IDocs and EDI. An IDoc (intermediate document) is SAP's standard format for exchanging business documents, such as ORDERS for purchase orders and ORDRSP for order confirmations. SAP's training describes EDI between companies as always asynchronous (SAP Learning). They are central in ECC and still used with S/4HANA.
RFC and BAPIs. A BAPI is a business-level function, such as "create sales order", that other systems call by remote function call (RFC), either waiting for the result or through queued variants (same source).
OData and SOAP APIs. Web APIs listed on the SAP Business Accelerator Hub, SAP's catalog. An SAP Learning lesson counts more than 2,700 APIs there, in REST, OData V2, OData V4 and SOAP (SAP Learning).
Business events. S/4HANA publishes standard events, and SAP's brokers deliver them: SAP Integration Suite, advanced event mesh, the smaller Event Mesh capability, and SAP Cloud Application Event Hub for SAP's cloud applications. For events from ECC, SAP's reference architecture points to SAP Application Interface Framework (AIF) (SAP Architecture Center).
Files. Banks are the classic case: SAP Multi-Bank Connectivity exchanges payments and statements with banks over SWIFT, API, SFTP or EBICS, mostly in ISO 20022 formats (SAP Learning).
SAP's own method for choosing, the Integration Solution Advisory Methodology (ISA-M), groups interfaces into styles (process, data, analytics, user and thing integration) and refines them into patterns (SAP Learning). The platform itself, SAP Integration Suite, is covered in the integration platform and clean core.
Is a person or a process waiting for the answer right now?
Yes, within seconds
Ask and wait (API call)
Must the sender keep working if the receiver is down?
Yes
Send and move on (queue, IDoc, EDI)
Do several systems need to know when something changes?
Yes
Events, with an API to read details
Is it large volumes for reporting, where an hour or a day is fine?
Yes
Batch file or bulk extraction
Then three checks, whatever the style:
Is it released? Use interfaces SAP has released and documented. Undocumented interfaces are used "at your own risk", as an ASUG summary of SAP's 2026 API policy puts it (ASUG).
What if it arrives twice, late or out of order? Someone must design for it.
Who fixes a failed message? Every interface needs a named owner who reprocesses errors.
Calder interface
Who waits?
Style chosen
Account manager checks a delivery date while quoting
The account manager, on the phone
Ask and wait
Distributors send purchase orders
Nobody; the order must not be lost
EDI, send and move on
CRM, data warehouse and an agent need order changes
"APIs are modern, IDocs are legacy, so replace all IDocs." IDocs are a solid asynchronous style and still used with S/4HANA. Replace an interface when it is unreleased or doesn't fit, not because of its age.
"Real time means synchronous." Events are near real time and don't make the sender wait. Synchronous calls make two systems fail together.
"The platform guarantees exactly once." Platforms can guarantee that a message is not lost. Recognizing a repeat is the receiver's job.
"One style for the whole program keeps it simple." It just moves the problems: forcing every interface into APIs creates fragile chains; forcing everything into files makes every process a day late.
"Agents need a new kind of integration." An AI agent is one more consumer. It uses the same APIs and events, with the same failures.
Pick one answer for each question. The explanation appears after you choose.
1What is the first question an integration lead asks to choose a style for a new interface?
Answer: B. Who waits, and how fast, decides between ask-and-wait and the asynchronous styles. Technology age and team habits come later, if at all.
2Calder's distributors send purchase orders, and an order must never be lost when S/4HANA is down for maintenance. Which style fits?
Answer: B. Nobody waits for the answer, and the sender must keep working when the receiver is down. A queued asynchronous message survives the outage; a synchronous call would fail with it.
3Why does building interfaces on released SAP interfaces matter to the business?
Answer: C. SAP describes released APIs as upgrade-stable. Interfaces built on undocumented objects must be retested, and sometimes rebuilt, at every upgrade.
4The CRM, the data warehouse and an AI agent all need to know when a sales order changes. What is the usual design?
Answer: C. Events tell many receivers at once without making S/4HANA wait for any of them. Polling wastes calls, and chained synchronous calls make S/4HANA depend on all three systems.
5A vendor says its platform "guarantees exactly-once delivery". What should you still ask?
Answer: D. A platform can make sure a message isn't lost, which often means it may be delivered twice. Only the receiver can recognize a repeat, by a message ID or a business key.
6What does every interface need besides a chosen style?
Answer: C. Messages will fail. Without a named owner and a place to see failures, they sit in error queues until a customer or supplier calls.
7Your team proposes a new interface style just for an AI agent that reads open orders. What is the sound response?
Answer: A. An agent fails in the same ways as any other consumer: timeouts, duplicates, stale data. It should use the same released interfaces, with the same controls.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Every interface answers two questions. Who waits? And what happens when the message is lost, repeated, late or out of order? The style follows from the first answer; most of the design work goes into the second.
flowchart LR
A[New interface] --> W{Someone waits<br/>for the answer?}
W -->|yes, seconds| S[Synchronous API<br/>OData, SOAP, RFC]
W -->|no| M{How many receivers,<br/>how fast?}
M -->|one receiver| Q[Asynchronous message<br/>IDoc, EDI, async SOAP]
M -->|many, minutes| E[Event + API read]
M -->|bulk, hours or a day| F[File or extraction]
Synchronous styles couple two systems in time: if the receiver is down or slow, the caller is too. Asynchronous styles decouple them, and pay for it with three new problems: duplicates, ordering and messages that can never be processed. The rest of this layer shows each style, breaks each one on purpose, and ends with a decision for ten Calder interfaces.
A BAPI is a business-level function in an SAP system, stored as an RFC-enabled function module. SAP's training describes BAPIs as the standard interfaces between SAP and non-SAP components, and the RFC variants that call them (SAP Learning):
sRFC: synchronous; both systems must be up.
aRFC: returns control at once, but the called system must still be up, so it is not truly asynchronous.
tRFC: stored under a transaction ID and executed once, even if the target is down at call time. It does not guarantee order.
qRFC: tRFC with queues, so units of work run in a set order.
bgRFC: the successor to tRFC and qRFC, with queues whose dependencies act like locks.
An IDoc carries one business document. It has a control record (sender, receiver, message type, last status), data records (segments in a hierarchy) and status records (every processing step and error, kept separately). A message type such as ORDERS or ORDRSP names the business meaning; partner profiles set how each partner's IDocs are processed. ALE (Application Link Enabling) distributes IDocs between systems; asynchronous BAPI calls also go through ALE with generated IDoc interfaces (same source).
The order-to-cash topic showed the pattern with SAP Business Network: purchase orders as ORDERS, confirmations as ORDRSP, invoices as INVOIC. Inside an SAP landscape, the warehouse topic showed decentralized EWM receiving master data by IDocs and transactional data by qRFC.
OData is a REST-based protocol with a standard query language ($filter, $select, $expand). S/4HANA's APIs come in OData V2 (the sales order API you called in your first SAP API) and OData V4; the Business Accelerator Hub lets you filter by version (SAP Learning). Check on the hub which version and successor an API has before you build; don't assume one is always available.
SOAP services exchange XML messages and exist in both synchronous and asynchronous variants. Journal entry posting is a good example: JournalEntryCreateRequestConfirmation_In answers at once, while JournalEntryBulkCreationRequest_In is asynchronous, with its status checked in the SAP Application Interface Framework (AIF) monitor; both need the communication scenario SAP_COM_0002 set up as a communication arrangement in S/4HANA Cloud (SAP blog). The same business object, two styles: choose by who waits and how many.
An event says "sales order 1000011 changed". A broker keeps a queue per subscriber and delivers to each. SAP's reference architecture positions events alongside APIs for real-time process integration, lists standard S/4HANA events on the Business Accelerator Hub, and notes custom events built with the ABAP RESTful application programming model (RAP) (SAP Architecture Center). The event-driven agents topic built a subscriber in detail.
Two delivery modes matter. In advanced event mesh, direct messaging is best effort and unacknowledged: events can be dropped during congestion or failures. Guaranteed messaging keeps an event on a durable endpoint until the consumer acknowledges it or it expires (advanced event mesh docs). Business events need guaranteed delivery; guaranteed delivery means a consumer can see the same event again after a lost acknowledgment.
Events come in two shapes. A notification carries little more than the key; the subscriber reads the current state by API. A data event carries the changed fields. Notifications plus a read are safer against ordering problems, because the read always returns the latest state, at the cost of one API call per event. Check each event's payload on the hub before you choose.
A file carries many records at once, on a schedule. It is the right style when volume is large, latency of hours is fine, and the partner works that way: banks, payroll providers, some carriers. Calder's bank statements come through SAP Multi-Bank Connectivity, which supports SWIFT, API or SFTP, and Host2Host or EBICS channels, with ISO 20022 as the standard format (SAP Learning). Bulk extraction to a data warehouse is the analytics cousin, covered in the reporting topic.
EDI is asynchronous messaging between companies, in standards such as ASC X12 and UN/EDIFACT. In SAP Integration Suite, Trading Partner Management holds partners and agreements and drives generic integration flows, with AS2 and IDoc senders and interchange numbering for X12, EDIFACT and IDoc (SAP guide). On the SAP side the message usually lands as an IDoc or an API call. The partner-facing part is the B2B layer; the SAP-facing part is one of the styles above.
Receiver remembers a message ID or business key (customer + customer PO) and returns the first result
Ordering
An older change overwrites a newer one
One queue per object (qRFC, queue per order), or a version on each message, or read the current state back
Retry with backoff
A short outage loses a message
Retry only what can succeed later (timeouts, 503), wait longer each time
Error queue
One bad message blocks or is lost
After a few tries, park it for a named person to fix and replay
You built the first, third and fourth in the Unit 5B setup. SAP's own exactly-once guidance combines guaranteed delivery with an idempotent receiver; that topic links it.
S/4HANA distinguishes released interfaces (remote APIs, events, local APIs such as released CDS views) from everything else. SAP calls released interfaces "upgrade-stable" and guarantees their stability from release to release; in ABAP Cloud, syntax and ABAP test cockpit checks block unreleased objects (SAP Learning).
SAP's 2026 API policy adds a commercial edge. ASUG's summary says published APIs used as documented are the supported path; undocumented interfaces are at the customer's own risk; and ODP-RFC, an internal extraction interface, has been technically blocked for non-compliant use since 9 June 2026, with an opt-out until 31 December 2026 (ASUG, 5 July 2026). Read the policy itself with your legal team before deciding on an affected interface; the first SAP API topic links it.
#Build it yourself: break a synchronous and an event-driven interface
You will run one synchronous interface and one event-driven interface on your own computer, break each in the ways real interfaces break, and switch on the design fix for each failure. One script does both; a summary table shows every case.
flowchart LR
subgraph sync[Synchronous]
CRM1[CRM] -->|POST order, waits 1.0 s| S4[S/4HANA stand-in<br/>127.0.0.1]
end
subgraph ev[Event-driven]
P[S/4HANA stand-in] -->|SalesOrder changed| B[(Broker:<br/>queue per subscriber)]
B --> CRM2[CRM]
B --> DW[Data warehouse]
end
Your course folder from Unit 1 and the unit5b folder from the Unit 5B setup.
Python 3.10 or later. No accounts, no internet, no cost.
About 30 minutes.
All orders, dates and messages are fictional Calder values. The event format is a course format shaped after CloudEvents (id, source, type, time, data); it is not SAP's event payload. Sales order 1000011 and its dates come from the exception trace: requested 13 November, slipped to 30 November when the motors were late, back to 19 November after expediting.
#Step 1: Open the course folder and turn on the virtual environment
Open VS Code, choose File > Open Folder and open orchestrate-course.
Open a terminal with Terminal > New Terminal.
Turn on the virtual environment if the prompt doesn't start with (.venv):
Create a new file, paste the script below, and save it as two_styles.py inside unit5b.
"""Two integration styles side by side, and how each one breaks (Unit 5B).
Calder Pumpworks is a fictional company. Order numbers, dates and the
message formats here are course values, not SAP standard values, and
nothing in this script is SAP software.
sync Calder's CRM creates a sales order in S/4HANA and waits for the
order number (request-reply over HTTP, on your own computer).
events S/4HANA publishes "sales order changed" events; a broker keeps a
queue per subscriber (CRM and the data warehouse).
Run from your course folder, for example:
python unit5b/two_styles.py sync --break slow
python unit5b/two_styles.py sync --break slow --fix
python unit5b/two_styles.py events --break out-of-order --fix
python unit5b/two_styles.py all (every case, as one table)
Only Python's standard library is used. Nothing goes over the internet: the
sync receiver listens on 127.0.0.1 on a free port and stops at the end.
"""
import argparse
import json
import socket
import threading
import time
import urllib.error
import urllib.request
import uuid
from datetime import date
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SYNC_BREAKS = ["none", "down", "slow"]
EVENT_BREAKS = ["none", "subscriber-down", "out-of-order", "duplicate", "poison"]
CLIENT_TIMEOUT_S = 1.0 # how long the CRM waits for an answer
SLOW_ANSWER_S = 2.0 # how long the slow receiver takes to answer
MAX_TRIES = 3 # event deliveries before a message is parked
QUOTE = {"SoldToParty": "10000001", "PurchaseOrderByCustomer": "BWU-PO-5633",
"Material": "ES-200", "Quantity": 16, "RequestedDate": "2026-11-13"}
# ----------------------------------------------------------------- sync ----
class Receiver:
"""Plays S/4HANA receiving a create-order call. Course paths, not SAP's."""
def __init__(self, mode):
self.mode = mode
self.orders = [] # sales orders created
self.seen_keys = {} # Idempotency-Key -> answer already given
self.calls = 0
self.lock = threading.Lock()
self.busy = threading.Event() # set while a slow answer is pending
self.server = None
def start(self):
receiver = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args): # keep the terminal quiet
pass
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
key = self.headers.get("Idempotency-Key")
with receiver.lock:
receiver.calls += 1
first_call = receiver.calls == 1
if key and key in receiver.seen_keys:
answer = receiver.seen_keys[key]
replay = True
else:
number = str(1000301 + len(receiver.orders))
receiver.orders.append({"SalesOrder": number, **json.loads(body)})
answer = {"SalesOrder": number}
replay = False
if key:
receiver.seen_keys[key] = answer
if receiver.mode == "slow" and first_call:
receiver.busy.set()
time.sleep(SLOW_ANSWER_S) # the order is saved; the answer is late
receiver.busy.clear()
data = json.dumps(answer).encode()
try:
self.send_response(201 if not replay else 200)
self.send_header("Content-Type", "application/json")
if replay:
self.send_header("Idempotent-Replay", "true")
self.end_headers()
self.wfile.write(data)
except (BrokenPipeError, ConnectionResetError):
pass # the caller stopped waiting; nobody hears the answer
self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=self.server.serve_forever, daemon=True).start()
return self.server.server_address[1]
def stop(self):
while self.busy.is_set(): # let a slow answer finish before counting
time.sleep(0.05)
if self.server:
self.server.shutdown()
self.server.server_close()
def free_port():
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def post_order(port, key):
req = urllib.request.Request(
f"http://127.0.0.1:{port}/calder/orders", method="POST",
data=json.dumps(QUOTE).encode(), headers={"Content-Type": "application/json"})
if key:
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req, timeout=CLIENT_TIMEOUT_S) as resp:
return resp.status, json.loads(resp.read()), resp.headers.get("Idempotent-Replay")
def run_sync(brk, fix, log=print):
"""Returns a result dict; prints what the CRM user would see."""
receiver = Receiver(brk)
port = free_port() if brk == "down" else receiver.start() # down: nothing listens
key = str(uuid.uuid4()) if fix else None # one key per business request
outbox = []
result = {"style": "sync", "break": brk, "fix": fix}
tries = 0
try:
while True:
tries += 1
try:
status, body, replay = post_order(port, key)
log(f" try {tries}: {status} sales order {body['SalesOrder']}"
+ (" (replay of the first answer, no new order)" if replay else ""))
result["user_sees"] = f"order {body['SalesOrder']}"
break
except (TimeoutError, socket.timeout):
log(f" try {tries}: no answer within {CLIENT_TIMEOUT_S:.1f} s (timeout)")
except urllib.error.URLError as exc:
if isinstance(exc.reason, (TimeoutError, socket.timeout)):
log(f" try {tries}: no answer within {CLIENT_TIMEOUT_S:.1f} s (timeout)")
else:
log(f" try {tries}: cannot connect ({type(exc.reason).__name__})")
if fix:
outbox.append({"key": key, **QUOTE})
result["user_sees"] = "queued in the outbox, will send later"
else:
result["user_sees"] = "error screen; the quote is not an order"
break
if tries >= 2:
result["user_sees"] = "error after 2 tries"
break
log(" the CRM retries the same request")
finally:
receiver.stop()
result["orders"] = len(receiver.orders)
result["outbox"] = len(outbox)
return result
# --------------------------------------------------------------- events ----
def make_event(seq, etype, confirmed):
"""A course event, shaped after CloudEvents (id, source, type, plus data)."""
return {"id": f"evt-{seq:04d}", "source": "/calder/s4/CP10", "type": etype,
"time": f"2026-10-2{seq}T09:00:00Z",
"data": {"SalesOrder": "1000011", "Version": seq, "ConfirmedDate": confirmed}}
EVENTS = [
make_event(1, "calder.salesorder.Created.v1", "2026-11-13"), # as requested
make_event(2, "calder.salesorder.Changed.v1", "2026-11-30"), # motors late
make_event(3, "calder.salesorder.Changed.v1", "2026-11-19"), # after expediting
]
TRUE_FINAL_DATE = "2026-11-19"
class Subscriber:
def __init__(self, name, fix):
self.name, self.fix = name, fix
self.order = {} # what this system believes about 1000011
self.notifications = 0 # e.g. emails to the account manager
self.seen_ids = set()
self.offline_for = 0 # deliveries to refuse before coming back
self.applied = 0
def handle(self, event):
if self.applied >= 1 and self.offline_for > 0: # goes down after event 1
self.offline_for -= 1
raise ConnectionError(f"{self.name} is offline")
data = event["data"]
date.fromisoformat(data["ConfirmedDate"]) # raises on a bad date
if self.fix and event["id"] in self.seen_ids:
return "duplicate ignored"
if self.fix and data["Version"] <= self.order.get("Version", 0):
self.seen_ids.add(event["id"])
return "older version ignored"
self.order = dict(data)
self.applied += 1
self.seen_ids.add(event["id"])
if event["type"].endswith("Changed.v1"):
self.notifications += 1
return f"applied, date {data['ConfirmedDate']}"
def run_events(brk, fix, log=print):
crm, dw = Subscriber("CRM", fix), Subscriber("DW", fix)
stream = [dict(e) for e in EVENTS]
if brk == "out-of-order":
stream = [stream[0], stream[2], stream[1]] # 3 overtakes 2
if brk == "duplicate":
stream = stream[:2] + [stream[1]] + stream[2:] # 2 redelivered
if brk == "poison":
bad = make_event(9, "calder.salesorder.Changed.v1", "2026-13-45")
stream = stream[:2] + [bad] + stream[2:]
guaranteed = not (brk == "subscriber-down" and not fix) # direct mode drops
queues = {crm.name: list(stream), dw.name: list(stream)}
dead = {crm.name: [], dw.name: []}
dropped = 0
if brk == "subscriber-down":
crm.offline_for = 2 # CRM is down for two deliveries after event 1
publisher_wait_s = 0.0 # the publisher never waits for subscribers
for sub in (crm, dw):
q = queues[sub.name]
tries = 0 # tries for the event at the head of the queue
while q:
event = q[0]
try:
outcome = sub.handle(event)
log(f" {sub.name:3} {event['id']} v{event['data']['Version']}: {outcome}")
q.pop(0)
tries = 0
except ConnectionError:
if guaranteed:
log(f" {sub.name:3} {event['id']}: CRM offline, event stays on its queue")
else:
log(f" {sub.name:3} {event['id']}: CRM offline, direct mode drops it")
dropped += 1
q.pop(0)
except ValueError:
tries += 1
log(f" {sub.name:3} {event['id']}: cannot read the date (try {tries})")
if tries < MAX_TRIES:
continue
if fix:
dead[sub.name].append(q.pop(0))
log(f" {sub.name:3} {event['id']}: parked on the dead-letter queue")
tries = 0
else:
log(f" {sub.name:3} stuck: the bad event blocks every event behind it")
q.clear() # in real life it is retried forever
return {"style": "events", "break": brk, "fix": fix,
"crm_date": crm.order.get("ConfirmedDate", "-"),
"dw_date": dw.order.get("ConfirmedDate", "-"),
"crm_notifications": crm.notifications,
"dropped": dropped, "dead_letters": len(dead["CRM"]) + len(dead["DW"]),
"publisher_wait_s": publisher_wait_s}
# ----------------------------------------------------------------- main ----
def verdict(r):
if r["style"] == "sync":
if r["orders"] > 1:
return f"BROKEN: {r['orders']} sales orders for one quote"
if r["orders"] == 0 and r["outbox"] == 0:
return "BROKEN: no order and nothing kept"
if r["outbox"]:
return "SAFE: kept in the outbox, sent when S/4HANA is back"
return "OK: one sales order"
problems = []
if r["crm_date"] != TRUE_FINAL_DATE:
problems.append(f"CRM shows {r['crm_date']}")
if r["dropped"]:
problems.append(f"{r['dropped']} event(s) lost")
if r["crm_notifications"] > 2:
problems.append(f"{r['crm_notifications']} emails for 2 changes")
if problems:
return "BROKEN: " + ", ".join(problems)
if r["dead_letters"]:
return "OK: the bad event is parked on each dead-letter queue for a person"
return f"OK: CRM and DW show {TRUE_FINAL_DATE}"
def main():
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("style", choices=["sync", "events", "all"])
p.add_argument("--break", dest="brk", default="none",
help="sync: none, down, slow. events: none, subscriber-down, "
"out-of-order, duplicate, poison")
p.add_argument("--fix", action="store_true", help="turn on the design fix")
a = p.parse_args()
if a.style == "all":
print(f"{'style':7} {'break':16} {'fix':4} result")
for style, breaks, fn in (("sync", SYNC_BREAKS, run_sync),
("events", EVENT_BREAKS, run_events)):
for brk in breaks:
for fix in (False, True):
if brk == "none" and fix:
continue
r = fn(brk, fix, log=lambda *_: None)
print(f"{style:7} {brk:16} {'yes' if fix else 'no':4} {verdict(r)}")
return
allowed = SYNC_BREAKS if a.style == "sync" else EVENT_BREAKS
if a.brk not in allowed:
p.error(f"--break for {a.style} must be one of: {', '.join(allowed)}")
print(f"{a.style}, break={a.brk}, fix={'on' if a.fix else 'off'}")
r = (run_sync if a.style == "sync" else run_events)(a.brk, a.fix)
if a.style == "sync":
print(f" CRM user sees: {r['user_sees']}")
print(f" sales orders in S/4HANA: {r['orders']} in the outbox: {r['outbox']}")
else:
print(f" CRM date {r['crm_date']}, DW date {r['dw_date']}, "
f"emails to the account manager {r['crm_notifications']}, "
f"dropped {r['dropped']}, dead letters {r['dead_letters']}, "
f"publisher waited {r['publisher_wait_s']:.0f} s")
print(f" {verdict(r)}")
if __name__ == "__main__":
main()
#Step 3: Run the synchronous interface, then break it
Run it as it should work:
python unit5b/two_styles.py sync
On Windows, python unit5b\two_styles.py sync works too. You should see:
sync, break=none, fix=off
try 1: 201 sales order 1000301
CRM user sees: order 1000301
sales orders in S/4HANA: 1 in the outbox: 0
OK: one sales order
Make the receiver slow. It saves the order but answers after 2 seconds, while the CRM waits only 1:
python unit5b/two_styles.py sync --break slow
sync, break=slow, fix=off
try 1: no answer within 1.0 s (timeout)
the CRM retries the same request
try 2: 201 sales order 1000302
CRM user sees: order 1000302
sales orders in S/4HANA: 2 in the outbox: 0
BROKEN: 2 sales orders for one quote
The retry looked reasonable, and it created a duplicate. Brennock would get 32 pumps instead of 16.
Switch on the fix, an Idempotency-Key header created once per quote:
sync, break=slow, fix=on
try 1: no answer within 1.0 s (timeout)
the CRM retries the same request
try 2: 200 sales order 1000301 (replay of the first answer, no new order)
CRM user sees: order 1000301
sales orders in S/4HANA: 1 in the outbox: 0
OK: one sales order
Now take the receiver down, without and with the fix:
python unit5b/two_styles.py sync --break down
python unit5b/two_styles.py sync --break down --fix
Without the fix the user sees error screen; the quote is not an order. With it, the request goes to an outbox (SAFE: kept in the outbox, sent when S/4HANA is back). Notice what the fix did: it turned a synchronous interface into an asynchronous one for the moment S/4HANA is down. If that happens often, the interface wasn't synchronous by nature.
#Step 4: Run the event-driven interface, then break it
Run it as it should work:
python unit5b/two_styles.py events
events, break=none, fix=off
CRM evt-0001 v1: applied, date 2026-11-13
CRM evt-0002 v2: applied, date 2026-11-30
CRM evt-0003 v3: applied, date 2026-11-19
DW evt-0001 v1: applied, date 2026-11-13
DW evt-0002 v2: applied, date 2026-11-30
DW evt-0003 v3: applied, date 2026-11-19
CRM date 2026-11-19, DW date 2026-11-19, emails to the account manager 2, dropped 0, dead letters 0, publisher waited 0 s
OK: CRM and DW show 2026-11-19
publisher waited 0 s: S/4HANA never waits for its subscribers. That is the point of events.
Take the CRM offline for two deliveries, first with direct (best-effort) delivery, then with guaranteed delivery:
Direct mode ends with BROKEN: CRM shows 2026-11-13, 2 event(s) lost. Guaranteed mode keeps the event on the CRM's queue (event stays on its queue) and ends with the right date.
Without the fix the CRM ends on 2026-11-30, the old date: the account manager tells Brennock the wrong day. The fix checks the Version on each event and ignores older ones (older version ignored).
Redeliver event 2, as a broker does after a lost acknowledgment, and then send a bad event:
The duplicate sends the account manager three emails for two changes; the fix remembers event IDs. The poison event has the date 2026-13-45. Without the fix it blocks every event behind it (stuck); with the fix it is parked after three tries and event 3 gets through.
style break fix result
sync none no OK: one sales order
sync down no BROKEN: no order and nothing kept
sync down yes SAFE: kept in the outbox, sent when S/4HANA is back
sync slow no BROKEN: 2 sales orders for one quote
sync slow yes OK: one sales order
events none no OK: CRM and DW show 2026-11-19
events subscriber-down no BROKEN: CRM shows 2026-11-13, 2 event(s) lost
events subscriber-down yes OK: CRM and DW show 2026-11-19
events out-of-order no BROKEN: CRM shows 2026-11-30
events out-of-order yes OK: CRM and DW show 2026-11-19
events duplicate no BROKEN: 3 emails for 2 changes
events duplicate yes OK: CRM and DW show 2026-11-19
events poison no BROKEN: CRM shows 2026-11-30
events poison yes OK: the bad event is parked on each dead-letter queue for a person
The run takes a few seconds because of the slow case. Every line is the same on every run: there is no randomness, and the timings are fixed in the script.
A small HTTP server on 127.0.0.1 playing S/4HANA. In slow mode it saves the order, then answers late. It remembers Idempotency-Key values and replays the first answer
post_order
The CRM's call, with a 1.0-second timeout, using Python's urllib
run_sync
Calls, retries once after a timeout, and with --fix sends one key per quote or keeps the request in an outbox when the receiver is down
EVENTS
Three course events for order 1000011, each with an ID and a version
Subscriber.handle
Applies an event; with --fix ignores repeated IDs and older versions; raises on a bad date
run_events
The broker: a queue per subscriber, direct or guaranteed delivery, three tries, then dead letters with --fix
Licensing: SAP Integration Suite, advanced event mesh and Multi-Bank Connectivity are licensed SAP services; check editions and pricing with SAP. This topic's code needs none of them. The platform and its editions are covered in the integration platform and clean core.
Where it sits. Calder's CP10 S/4HANA system is the hub. ECC CP20 talks to it by IDoc-based EDI. Partners reach it through a B2B layer, the CRM and an agent through APIs and events, the bank through Multi-Bank Connectivity, and the warehouse through extraction.
flowchart LR
BN[Business Network] <-->|IDoc ORDERS, ORDRSP| S4[S/4HANA CP10]
DIST[Distributors EDI] -->|B2B to IDoc or API| S4
ECC[ECC CP20] <-->|IDoc EDI| S4
S4 -->|events| BR[(Event broker)]
BR --> CRM[CRM]
BR --> AG[Agent on BTP]
CRM -->|OData ATP read| S4
BANK[Bank] -->|ISO 20022 file| S4
S4 -->|extraction| DW[Data warehouse]
Non-functional requirements that decide the style.
Integration and identity. System-to-system interfaces use technical communication users, scoped per communication arrangement. An agent or app acting for a person should carry that person's identity where possible, so SAP authorizations apply (grounding with authorizations). Every message carries a business key (customer and customer PO, sales order number) so receivers can detect duplicates and an owner can trace it.
The decision to record.
Context: the CRM shows confirmed dates; today a nightly file updates it, so dates are up to a day old.
Options: A, CRM calls an S/4HANA API on every screen open; B, S/4HANA calls the CRM on every order save; C, sales order events to a broker, CRM subscribes and reads the current order by API.
Decision: C, with guaranteed delivery, version checks and an API read for the current state.
Consequences: the CRM is current within minutes and S/4HANA never waits for it; Calder runs a broker and owns a dead-letter queue (Sales IT). Option A stays for the quoting screen, where a person waits.
What a design review will ask.
Which interfaces are synchronous, and does a person really wait for each one?
For each asynchronous interface, which key detects duplicates and what keeps the order?
Is any interface built on something SAP has not released or documented?
Who sees failed messages, in which tool, and how fast must they act?
What changes for the ECC interfaces when CP20 moves into CP10?
An AI agent is one more consumer. Through MCP or a direct API client it calls the same OData services and subscribes to the same events, so it inherits every failure in this topic: timeouts, retries that duplicate, stale data from out-of-order events. Design its tools like any interface: read through released APIs, use idempotency keys on anything that writes, and keep changes behind human approval, as in tool design for agents.
AI also helps with integration work itself: explaining a failed IDoc's status records in plain words, drafting a mapping between two formats, or grouping a night's error queue by cause. Those are reading and drafting tasks; a person still decides and reprocesses. Choosing the style for an interface is not an AI task: it follows from four facts per interface, which a checklist handles better.
Security. Communication users per arrangement, least privilege, keys in a secret store. EDI partners authenticate at the B2B layer (certificates for AS2).
Authorizations. A technical user that can create sales orders for every sales organization is a fraud risk; scope it. See roles and authorization design.
Monitoring. Every error queue has an owner, an alert and a target time. IDoc errors live in status records; asynchronous SOAP errors in AIF; events in the broker's dead-letter queues.
Volume. Size for month-end peaks. A synchronous API under a rate limit returns 429; senders must back off.
Cost. Each API call, event and message may count against platform metrics. Polling is the most expensive way to learn nothing changed.
Clean core. Released interfaces only. Record any exception with a replacement plan; the fit-to-standard topic shows how to log it.
Evaluation. Test the failure cases, not just the happy path: receiver down, slow, duplicate, out of order, bad data. The script in this topic is the checklist.
Synchronous chains. CRM calls S/4HANA, which calls a pricing service, which calls a tax service: the slowest link sets everyone's response time, and any outage stops all of them.
Retrying without idempotency. A timeout doesn't mean failure; the receiver may have done the work.
Retrying the unretryable. A 400 for an unknown customer will fail forever; park it.
Trusting event order. Even with queues, a redelivery or a second consumer can reorder events. Use versions or read back.
Direct delivery for business events. Best effort is fine for telemetry, not for orders.
Error queues without owners. The most common hypercare finding.
Polling for changes. It costs calls and still misses changes between polls.
Rebuilding working IDoc interfaces for fashion. Migrate when the interface is unreleased, doesn't fit, or blocks the target architecture.
#Exercise: choose the pattern for ten Calder interfaces
You will decide the style for ten Calder interfaces and check your decisions with a small script. The result is the start of the interface inventory in the next topics of this unit.
Before you start: have the Unit 5B setup done and two_styles.py from above saved.
Save the script below as check_patterns.py inside unit5b.
"""Check an integration pattern decision for Calder's interfaces (Unit 5B).
Calder Pumpworks is fictional. Interface IDs, volumes and the rules below
are course values, written to teach the decision, not SAP rules.
python unit5b/check_patterns.py --template write unit5b/calder_patterns.csv
python unit5b/check_patterns.py --sample check a built-in example with mistakes
python unit5b/check_patterns.py unit5b/calder_patterns.csv check your decisions
The template holds the facts for ten interfaces (who sends, who waits, how
many a day, how late is too late). You fill the decision columns.
"""
import argparse
import csv
import sys
from pathlib import Path
PATTERNS = {"sync-api", "async-message", "event", "file-batch", "b2b-edi", "extraction"}
FACTS = ["id", "sender", "receiver", "content", "per_day", "someone_waits",
"max_delay", "order_matters", "ecc_involved"]
DECISION = ["pattern", "technology", "released", "duplicates", "ordering", "error_owner"]
# Course facts. per_day is a rough course volume; max_delay is how late the
# receiver may learn about it before the business notices.
ROWS = [
["I01", "SAP Business Network", "S/4HANA CP10", "POs out; confirmations and ship notices in", "120", "no", "1h", "yes", "no"],
["I02", "S/4HANA CP10", "MES Rockford", "Released production orders out; confirmations back", "300", "no", "5min", "yes", "no"],
["I03", "S/4HANA CP10", "CRM", "Sales order status and confirmed dates", "450", "no", "15min", "yes", "no"],
["I06", "TM in CP10", "Carriers", "Freight orders and tenders; carrier invoices back", "40", "no", "1h", "no", "no"],
["I07", "S/4HANA CP10", "Data warehouse", "Open orders and billing for the revenue forecast", "15000", "no", "1d", "no", "no"],
["I08", "Bank", "S/4HANA CP10", "Bank statements", "1", "no", "1d", "yes", "no"],
["I09", "S/4HANA CP10", "Agent on BTP", "Exceptions to watch; reads current state", "200", "no", "15min", "no", "no"],
["I10", "Distributors (Hallam and others)", "S/4HANA CP10", "Customer purchase orders", "80", "no", "1h", "no", "no"],
["I12", "CRM", "S/4HANA CP10", "Availability check while quoting: can we confirm 16 ES-200 by a date?", "60", "yes", "2s", "no", "no"],
["I11", "S/4HANA CP10", "ECC CP20", "Intercompany orders; CP20 invoices back", "20", "no", "4h", "no", "yes"],
]
SAMPLE = { # an example answer with deliberate mistakes, for --sample
"I03": ["sync-api", "OData V2 call from S/4HANA on every save", "yes", "", "", "Sales IT"],
"I07": ["extraction", "ODP-RFC from the warehouse", "yes", "", "", "Analytics team"],
"I10": ["b2b-edi", "EDI via B2B platform to an inbound order", "yes", "", "", ""],
"I12": ["event", "Sales order events to the CRM", "yes", "event ID", "", "Sales IT"],
"I11": ["event", "S/4HANA business events to ECC", "yes", "event ID", "version", "Intercompany team"],
}
DELAY_S = {"2s": 2, "5min": 300, "15min": 900, "1h": 3600, "4h": 14400, "1d": 86400}
def check_row(r):
"""Return a list of (level, message) for one interface row."""
out = []
pat = r.get("pattern", "").strip()
if not pat:
return [("MISSING", "no pattern chosen")]
if pat not in PATTERNS:
return [("FAIL", f"pattern '{pat}' is not one of {', '.join(sorted(PATTERNS))}")]
if not r.get("technology", "").strip():
out.append(("MISSING", "name the technology (OData, IDoc, SOAP, file, events...)"))
if r.get("released", "").strip().lower() != "yes":
out.append(("FAIL", "the SAP side must use a released, documented interface"))
if "odp-rfc" in r.get("technology", "").lower():
out.append(("FAIL", "ODP-RFC is restricted under SAP's API policy; use a permitted extraction path"))
if not r.get("error_owner", "").strip():
out.append(("FAIL", "no error owner: who reprocesses a failed message?"))
waits = r["someone_waits"] == "yes"
per_day = int(r["per_day"])
if pat == "sync-api" and not waits:
out.append(("WARN", "synchronous, but nobody waits for the answer: the sender now "
"depends on the receiver being up; consider async or events"))
if waits and pat != "sync-api" and DELAY_S.get(r["max_delay"], 9e9) < 60:
out.append(("WARN", "a person waits for the answer; async can't reply in time"))
if pat in {"async-message", "event", "b2b-edi", "file-batch"} and not r.get("duplicates", "").strip():
out.append(("FAIL", "asynchronous delivery repeats messages: say how duplicates are detected"))
if r["order_matters"] == "yes" and pat in {"async-message", "event"} and not r.get("ordering", "").strip():
out.append(("FAIL", "order matters: say how ordering is kept (queue, version, read back)"))
if r["ecc_involved"] == "yes" and pat == "event":
out.append(("WARN", "ECC is involved: events there need an add-on such as SAP "
"Application Interface Framework; or use IDocs until the rollout"))
if pat == "extraction" and per_day < 1000:
out.append(("WARN", "low volume for an extraction; an API read may be simpler"))
if pat == "file-batch" and DELAY_S.get(r["max_delay"], 0) < 3600:
out.append(("WARN", "a batch file can't meet a delay under an hour"))
return out or [("OK", "")]
def write_template(path):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(FACTS + DECISION + ["notes"])
for row in ROWS:
w.writerow(row + [""] * (len(DECISION) + 1))
print(f"Wrote {path} with {len(ROWS)} interfaces. Fill the columns {', '.join(DECISION)}.")
def load(path):
with path.open(newline="", encoding="utf-8-sig") as f:
rows = list(csv.DictReader(f))
missing = [c for c in FACTS + DECISION if c not in (rows[0] if rows else {})]
if missing:
sys.exit(f"{path}: missing columns {', '.join(missing)}. Run --template for a fresh file.")
return rows
def main():
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("csvfile", nargs="?", help="your filled calder_patterns.csv")
p.add_argument("--template", action="store_true", help="write a blank template")
p.add_argument("--sample", action="store_true", help="check a built-in example")
a = p.parse_args()
if a.template:
write_template(Path("unit5b") / "calder_patterns.csv")
return
if a.sample:
rows = [dict(zip(FACTS + DECISION, r + SAMPLE.get(r[0], [""] * 6))) for r in ROWS]
elif a.csvfile:
rows = load(Path(a.csvfile))
else:
p.error("give a CSV file, or --template, or --sample")
counts = {}
for r in rows:
for level, msg in check_row(r):
counts[level] = counts.get(level, 0) + 1
label = f"{r['id']} {r.get('pattern') or '-':13}"
print(f"{level:8} {label} {msg}".rstrip())
print("\n" + " ".join(f"{k} {v}" for k, v in sorted(counts.items())))
if counts.get("FAIL") or counts.get("MISSING"):
sys.exit(1)
if __name__ == "__main__":
main()
See how the checker reports mistakes, on a built-in example:
python unit5b/check_patterns.py --sample
MISSING I01 - no pattern chosen
MISSING I02 - no pattern chosen
WARN I03 sync-api synchronous, but nobody waits for the answer: the sender now depends on the receiver being up; consider async or events
MISSING I06 - no pattern chosen
FAIL I07 extraction ODP-RFC is restricted under SAP's API policy; use a permitted extraction path
MISSING I08 - no pattern chosen
MISSING I09 - no pattern chosen
FAIL I10 b2b-edi no error owner: who reprocesses a failed message?
FAIL I10 b2b-edi asynchronous delivery repeats messages: say how duplicates are detected
WARN I12 event a person waits for the answer; async can't reply in time
WARN I11 event ECC is involved: events there need an add-on such as SAP Application Interface Framework; or use IDocs until the rollout
FAIL 3 MISSING 5 WARN 3
Write a blank template for your own decisions:
python unit5b/check_patterns.py --template
You should see Wrote unit5b/calder_patterns.csv with 10 interfaces.
Open unit5b/calder_patterns.csv in VS Code or a spreadsheet. The first nine columns are the facts. For each row, fill:
pattern: one of sync-api, async-message, event, file-batch, b2b-edi, extraction.
technology: what carries it (for example IDoc ORDERS via Business Network, OData read API, ISO 20022 file via Multi-Bank Connectivity).
released: yes only if the SAP side uses a released or documented interface.
duplicates: the key a receiver uses to spot a repeat, for every asynchronous row.
ordering: how order is kept, where order_matters is yes.
error_owner: the team that reprocesses failures.
notes: one line on why, especially for any WARN you accept.
Save the file as CSV (in Excel: File > Save As > CSV UTF-8) and check it:
Fix every FAIL and MISSING. For each WARN, change the decision or write in notes why you accept it.
Commit the CSV and the script with Git.
Done when: the checker prints no FAIL or MISSING for all ten interfaces, every remaining WARN has a reason in notes, and I12 (the account manager's availability check) is the only synchronous interface, or you can explain in notes why another one is.
For readers who want the theory behind delivery guarantees and SAP's own design method.
Exactly once is a combination, not a feature. Networks can lose a reply after the work is done, so a sender can never know for sure. Systems achieve "effectively once" by at-least-once delivery plus an idempotent receiver. The tRFC design in SAP's own stack, a transaction ID executed once, is the same idea (SAP Learning).
Ordering has a scope. Global order across all messages doesn't scale; order per object (per sales order, per purchase order) usually does. qRFC queues and per-object versions both implement ordering per key.
Guaranteed delivery and acknowledgments. The advanced event mesh documentation explains how the broker acknowledges a publisher and removes a message only after every consumer acknowledges it (docs). That second acknowledgment is where redeliveries come from.
ISA-M in depth. SAP's methodology turns style choices into a reusable integration strategy, with templates for documenting patterns and technology choices (SAP Learning). It is worth reading before you write an integration strategy for a program.
Pick one answer for each question. The explanation appears after you choose.
1In the slow case, the CRM's retry created a second sales order. Why?
Answer: A. A timeout means the caller stopped waiting, not that the receiver failed. The first order was saved, so the retry without an idempotency key created a duplicate.
2How does the --fix stop the duplicate in the synchronous case?
Answer: D. The key identifies the business request across tries. The receiver recognizes it and returns the stored answer instead of creating a new order. Longer timeouts only make the problem rarer.
3Which RFC variant runs a call exactly once even if the target system is down at call time, but does not guarantee order?
Answer: B. tRFC stores the call under a transaction ID and executes it once. qRFC adds queues to keep order; sRFC and aRFC need the target system to be up.
4Event 3 arrived before event 2, and the CRM ended on the older date. Which fix does the script apply?
Answer: B. A version per object lets the subscriber reject stale changes. Reading the current state back by API is the other common fix; redelivery and direct mode don't help.
5Why is direct (best-effort) delivery wrong for Calder's sales order events?
Answer: B. The advanced event mesh documentation describes direct messaging as best effort and unacknowledged. In the script, the CRM lost two changes while it was down and kept showing 13 November.
6A bad event with an impossible date keeps failing. What should the broker do after a few tries?
Answer: C. Retrying forever blocks every event behind it; dropping it loses data. A dead-letter queue keeps it for a person to fix and replay while later events flow.
7Calder's CP20 still runs ECC. A team proposes S/4HANA-style business events for the intercompany flow from CP20 now. What do you raise?
Answer: A. SAP's reference architecture names AIF for events from ECC and older S/4HANA releases. Until the rollout, the documented IDoc-based EDI flow is the simpler choice.
8An AI agent will read open sales orders and draft changes for a buyer. What does this topic say about its integration?
Answer: C. An agent is one more consumer with the same failures: timeouts, duplicates, stale data. It reads through released interfaces and keeps writes behind idempotency keys and human approval.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Orchestrate Lab is a free practice ERP in your browser, built around the fictional Calder Pumpworks. These are the places in it where you do what this topic teaches.
ChapterShop floorRun Rockford's floor from its own system: released production orders become dispatch lists per work center, in operation order with their scheduled dates, hours and the parts to issue; operators start and finish each operation by shift and confirm it with the good quantity, the scrap and its reason, and the hours; cheap parts are backflushed by the confirmation; the test bench's machine time is costed apart from the technician's; every pump gets a serial number and its own bench results; a pump that fails twice is repaired on its own rework order; every stop gets a reason, each work center's week reads as OEE, a breakdown goes to the maintenance planner, and in the end the floor runs its own system, fed by messages from the core, including cancellations the floor must agree to, and sending its confirmations back the same way, each of which can fail and must be fixed.
Stages build on each other: a stage link opens the core path at that stage. To start there without doing the earlier stages yourself, use Do this stage for me on the stage before it. A chapter starts inside your own company once the core path is done (or the lab finishes the core for you first); drills and exercises start from their own prepared copy. Everything is saved in this browser only.
Designing Event-Driven Applications (SAP Architecture Center, updated 10 October 2025)— Events alongside APIs for real-time process integration; SAP Integration Suite advanced event mesh, Event Mesh capability for small volumes, SAP Cloud Application Event Hub; standard S/4HANA events, custom events with RAP; older S/4HANA and ECC through AIF; events listed on the Business Accelerator Hub
Guidelines for API Journal Entry Post (SAP blog)— Synchronous SOAP JournalEntryCreateRequestConfirmation_In and asynchronous JournalEntryBulkCreationRequest_In; status in the AIF monitor and Message Dashboard; communication scenario SAP_COM_0002