Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

How SAP systems talk: integration styles and when to use each

Know IDocs, RFC and BAPIs, OData, SOAP, events and files, how each one fails, and choose the right style for every new interface.

Updated Oct 29, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

The decision guide

Four questions decide most interfaces:

Question If the answer is... Lean towards
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:

  1. 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).
  2. What if it arrives twice, late or out of order? Someone must design for it.
  3. 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 Nobody; three receivers Events, plus an API read
Bank statements Nobody; daily File

Questions to ask

  • For each interface: who waits for it, how many messages a day, and how late is too late?
  • Which interfaces use something SAP has not released or documented? What is the plan to replace it?
  • How does each receiver recognize a duplicate? Which business key does it use?
  • Who owns error handling for each interface, and where do they see failed messages?
  • For ECC systems in the landscape, which interfaces stay on IDocs until migration, and which move to APIs or events?
  • When an AI agent reads or writes SAP data, which interfaces does it use, and are they the same released ones?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • Interface: one defined exchange between two systems.
  • Synchronous: the caller waits for the answer.
  • Asynchronous: the sender hands over a message and continues; delivery happens later.
  • IDoc: SAP's intermediate document format for exchanging business documents.
  • BAPI / RFC: a business function in an SAP system, and the remote call used to run it.
  • OData: a web API standard SAP uses for most of its S/4HANA APIs, in versions V2 and V4.
  • Event: a message saying something happened, delivered to subscribers through a broker.
  • Idempotent: processing the same message twice has the same effect as once.
  • Released interface: one SAP has published and keeps stable across upgrades.

Check yourself

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.
Deep layer · 40 min read

Mental model: two questions per interface

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.

How it works

The SAP styles at a glance

Style SAP technology Waits? Typical use Fails by
Remote call RFC and BAPIs sRFC yes; tRFC, qRFC, bgRFC no SAP-to-SAP, ECC integrations Receiver down (sRFC); stuck queue (qRFC)
Business document IDoc over ALE or EDI No Orders, confirmations, invoices, master data Errors in status records nobody watches
Web API OData V2 and V4, REST Yes Apps, CRM, agents reading and creating objects Timeouts, overload, retries that duplicate
Web service SOAP, synchronous or asynchronous Either Finance postings, B2B, older integrations As above, by variant
Event Business events through a broker No Telling many systems about a change Lost (direct mode), out of order, repeated
File and batch Files over SFTP, bank channels, extraction No Bank statements, payments, analytics Late, partial or doubly loaded files

RFC, BAPIs and IDocs

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 and SOAP

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.

Events

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.

Files and batch

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 with customers, suppliers and carriers

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.

Idempotency, ordering, retries and error queues

Four mechanisms, whatever the style:

Mechanism Problem it solves How
Idempotency A retry or redelivery creates a second order 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.

Released versus unreleased

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

Before you start: complete Set up your computer for this course and Set up for Unit 5B. You need the orchestrate-course folder with its .venv and a unit5b folder. This topic installs no libraries.

What you need

  • 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

  1. Open VS Code, choose File > Open Folder and open orchestrate-course.
  2. Open a terminal with Terminal > New Terminal.
  3. Turn on the virtual environment if the prompt doesn't start with (.venv):
    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate

Step 2: Create the script

  1. 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

  1. 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
  2. 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.

  3. Switch on the fix, an Idempotency-Key header created once per quote:

    python unit5b/two_styles.py sync --break slow --fix
    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
  4. 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

  1. 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.

  2. Take the CRM offline for two deliveries, first with direct (best-effort) delivery, then with guaranteed delivery:

    python unit5b/two_styles.py events --break subscriber-down
    python unit5b/two_styles.py events --break subscriber-down --fix

    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.

  3. Deliver event 3 before event 2:

    python unit5b/two_styles.py events --break out-of-order
    python unit5b/two_styles.py events --break out-of-order --fix

    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).

  4. Redeliver event 2, as a broker does after a lost acknowledgment, and then send a bad event:

    python unit5b/two_styles.py events --break duplicate
    python unit5b/two_styles.py events --break duplicate --fix
    python unit5b/two_styles.py events --break poison
    python unit5b/two_styles.py events --break poison --fix

    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.

Step 5: See every case in one table

python unit5b/two_styles.py all
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.

Step 6: Save your work

git add unit5b/two_styles.py
git commit -m "Unit 5B: two integration styles and their failure modes"

What each part of the code does

Part What it does
Receiver 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
verdict Turns a result into OK, SAFE or BROKEN

If something goes wrong

What you see What it means What to do
python: command not found or the Microsoft Store opens Python isn't on your PATH Use python3 on macOS/Linux or py on Windows; see the Unit 1 setup
No such file or directory: unit5b/two_styles.py You are in the wrong folder cd into orchestrate-course first
ModuleNotFoundError You saved only part of the script The script needs no libraries; paste it again in full
--break for events must be one of: ... The break name doesn't match that style Use the names in the message
A firewall prompt about Python Your security software saw a local server Allow it for private networks, or ask IT; it listens only on 127.0.0.1
The slow case shows 1 order without --fix Your computer paused the server thread Run it again; if it repeats, close other heavy programs
A proxy or network error Shouldn't happen: nothing leaves your computer Check that the script wasn't changed to a non-local address

The SAP way

Need What SAP provides (as of October 2026) Notes
Find a released interface SAP Business Accelerator Hub: APIs, events, CDS views, RAP BO interfaces and BAdIs (SAP Learning) Check edition, version (OData V2 or V4), state and successor for each API
Expose an API from S/4HANA Cloud Communication scenarios and arrangements, such as SAP_COM_0002 for journal entry posting (SAP blog) The first SAP API topic covers communication management
Asynchronous SOAP monitoring SAP Application Interface Framework monitor and the Message Dashboard app (same source) Gives the error queue a business owner
SAP-to-SAP and ECC IDocs over ALE, RFC and BAPIs, bgRFC (SAP Learning) Documented, mature, still used with S/4HANA
Events Advanced event mesh; Event Mesh capability for starter volumes; SAP Cloud Application Event Hub for SAP cloud applications (SAP Architecture Center) Use guaranteed delivery for business events (docs)
EDI with partners Trading Partner Management and B2B flows in SAP Integration Suite (SAP guide) Check that your Integration Suite edition and contract include it
Banks SAP Multi-Bank Connectivity (SAP Learning) Check licensing with SAP
Agents MCP gateway in SAP Integration Suite (Enhanced and Premium editions, per ASUG, July 2026) See MCP and SAP tools for agents
Choose styles systematically ISA-M styles and patterns (SAP Learning) Free method and templates

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.

Build vs. SAP

Situation Build it yourself Use SAP's tooling
Learning the patterns, testing failure handling The local simulation in this topic Not needed
One app calling one S/4HANA API Direct call with a solid client (timeouts, retries, idempotency) API Management when you need policies, keys, rate limits for many consumers
Many receivers of order changes A home-made broker is a product to maintain Advanced event mesh or Event Mesh
EDI with dozens of partners Hand-built mappings per partner don't scale Trading Partner Management and B2B flows, or an established EDI provider
Error handling with business owners Custom queues need a monitoring UI and roles AIF, Cloud Integration monitoring
Agents reading SAP data A custom MCP server: your security burden The MCP gateway where licensed, still on released APIs

Architecture view

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.

Requirement Typical target What breaks if missed
Latency for a waiting user Answer within 2 s The account manager stops trusting the screen
Decoupling from outages Sender unaffected by receiver maintenance Orders lost during a system update
Duplicates Zero business duplicates Double shipments, credit notes
Ordering Latest state wins per object Stale dates promised to customers
Volume Month-end peak, not average (see non-functional design) Rate limits, queues backing up
Upgrade stability Released interfaces only Retest and rebuild at each upgrade

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.

  1. Which interfaces are synchronous, and does a person really wait for each one?
  2. For each asynchronous interface, which key detects duplicates and what keeps the order?
  3. Is any interface built on something SAP has not released or documented?
  4. Who sees failed messages, in which tool, and how fast must they act?
  5. What changes for the ECC interfaces when CP20 moves into CP10?

Where AI fits, and where it doesn't

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.

Production concerns

  • 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.

Pitfalls

  • 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.

  1. 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()
  1. 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
  2. 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.

  3. 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.
  4. Save the file as CSV (in Excel: File > Save As > CSV UTF-8) and check it:

    python unit5b/check_patterns.py unit5b/calder_patterns.csv
  5. Fix every FAIL and MISSING. For each WARN, change the decision or write in notes why you accept it.

  6. 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.

Going further

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.

Check yourself

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.

Practice this in the lab

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.
  • DrillStill building a cancelled orderProduction, about 7 minutes
  • DrillThe test the books never heard aboutProduction, about 6 minutes

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.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in