OData Batch Requests and Deep Inserts Explained
A batch request bundles several OData operations into one HTTP call, with changesets as the atomic write unit inside it. A deep insert is a payload shape: creating a root entity and its child entities in a single nested JSON structure via navigation properties. They are often used together but solve different problems, and confusing the two is the most common source of bugs in create-heavy Fiori apps.
This page separates two frequently conflated OData mechanics: batching, which is a transport and atomicity technique, and deep insert, which is a payload structure for creating a parent and its children in one call. It covers where each shows up in a RAP or Gateway service, how they interact with draft handling and late numbering, and what actually breaks in production when they are misused.
Published 16 Sept 2026· 1,598 words
What it is
A batch request is a single HTTP call carrying multiple OData operations, split into one or more changesets. Everything inside one changeset commits or rolls back together; different changesets in the same batch are independent of each other and of any plain GET requests riding alongside them. A deep insert is a different thing entirely: it is the shape of a single create payload, where a root entity's JSON or Atom body contains its dependent entities nested under a navigation property, so the server creates parent and children as one logical operation instead of a create followed by several update-the-association calls. The structural fact that causes most confusion is that a deep insert can be sent as one flat request or as part of a batch changeset referencing newly created entities by content ID, and people assume batching and deep insert are the same mechanism when they are orthogonal: one is about transport atomicity, the other is about payload nesting.
When to use it
Deep insert is right when the business object is genuinely composed: a sales order and its items, a header and its address, anything the UI presents and saves as one unit. It is wrong when the child entities have independent lifecycles or need their own authorization checks before the parent exists, because a deep create forces them through the parent's create handler. Batching is right whenever the UI must issue several logically related calls in one round trip, most obviously a Fiori Elements list report saving several new rows plus a few updates at once. Batching is the wrong tool when it is used purely to cut HTTP overhead for independent read requests that would be better served by requesting more fields or a wider expand in a single GET, or by proper server-side paging. Reaching for a giant batch of unrelated changesets to avoid several small round trips usually trades one problem for a harder one: a single slow or locked operation now stalls the whole batch response.
How it fits the stack
Batch requests sit at the protocol layer, above the RAP behavior implementation and below the UI's model layer; the Fiori Elements framework and SAPUI5's OData model construct batches automatically based on how many pending changes exist in the client-side change set. Deep insert sits one layer down from that: once a batch changeset reaches the service, the deep create payload is unpacked by the OData layer and mapped onto RAP create operations through the CDS composition and the associations marked as 'create by association' in the behavior definition. Underneath that, the transactional buffer in the RAP runtime treats the whole deep structure as one save unit. Deep insert supersedes the older pattern of issuing a create for the header, waiting for the response key, then issuing separate creates for each item referencing that key by hand; that pattern still exists for unmanaged scenarios or classic Gateway services without behavior-based composition.
A worked example
A travel booking business object has a root entity for the travel header and a composition child for bookings. The behavior definition marks the association from travel to booking as usable for deep create, and both entities have their create operations implemented in the behavior pool, with the booking's create handler reading the parent key out of the mapped association rather than expecting it in the payload. In the Fiori Elements object page, a user creates a new travel and adds three booking rows before pressing save. The UI model accumulates all of this as one pending change set and sends a single $batch request containing one changeset, itself containing one deep insert POST for the travel entity whose JSON body has an array of three booking objects nested under the navigation property. The RAP save sequence executes determinations, validations and numbering for travel and its three bookings inside one transaction, and a single combined response comes back, or a single combined rollback if any booking fails validation.
How to choose
- Composition versus independent entities: if the child can be created, updated or deleted without its parent existing yet, do not model it as a deep-create composition child; give it its own create operation and let the UI issue separate calls, otherwise every child creation is held hostage to the parent's validations.
- Numbering strategy: deep create with database-generated parent keys usually requires late numbering so the children can be persisted with a temporary reference before the real parent key exists; check this before modeling the composition, not after the first failed deep insert in testing.
- Depth of nesting: most behavior-based deep create support is reliable for one level, root plus direct children; a grandchild nested three levels deep in one payload is a much less common and less well exercised path, and is worth avoiding unless there is no alternative.
- Batching for atomicity versus batching for volume: use a changeset when the operations must succeed or fail together; do not use a large batch of independent changesets as a substitute for bulk load tooling, since each changeset still runs the full behavior stack including determinations and validations per entity.
- Draft interaction: if the business object has draft handling, decide whether the deep insert should populate the draft table directly or go through activation, since the two paths have different validation timing and different failure behavior.
- Error granularity the UI needs: a batch response reports success or failure per changeset and per request inside it, not per nested child inside a single deep insert; if the UI needs to tell the user exactly which of five nested rows failed, that has to be built into the error messages returned by the handler, not assumed from the protocol.
Common pitfalls
- Putting a GET request inside a changeset; changesets are for write operations only, and a GET mixed in causes a batch-level error that looks unrelated to the actual request that failed.
- Forgetting that a changeset is atomic but the batch as a whole is not; a batch with three changesets can return two successes and one failure, and UI code that assumes an all-or-nothing outcome for the entire batch will report the wrong result to the user.
- Deep insert failing silently on the child side because the association was not exposed for create, so the framework accepts the parent and drops the nested children without a clear error, which then looks like a data loss bug rather than a modeling gap.
- Late numbering omitted from the setup, so a deep create that works in unit tests with a single row fails once concurrent users create parent and child rows at volume, because two children end up racing for the same temporary key resolution.
- Treating a large batch as a performance optimization for read scenarios; batching does not reduce backend processing cost, it only reduces the number of HTTP round trips, so a batch full of expensive nested GETs is still expensive, just harder to profile because it is one browser network entry instead of many.
- ETag and concurrency checks on deep update payloads being skipped because the nested child rows do not carry their own ETag, leading to lost-update situations that only surface once two users edit the same composed object at the same time.
ECC, S/4HANA and clean core
Deep insert and batching are standard OData mechanics and remain fully supported in RAP-based services on S/4HANA; there is nothing here that is deprecated or discouraged under clean core, since both are protocol-level behavior rather than a custom extension technique. The clean-core relevance is indirect: when extending a standard business object to support deep create for a custom child entity, the extension must use the released composition and association extension points rather than modifying the standard behavior definition directly, otherwise the deep create logic breaks on the next upgrade. Classic Gateway-generated services (SEGW) that hand-code deep insert logic in the data provider class carry more upgrade risk than RAP-managed composition, since the RAP framework handles the deep create sequencing generically and the developer only supplies the entity-specific validations and determinations.
Whose problem this is
This is developer territory for the behavior implementation and association modeling, with architect input on whether a relationship should be modeled as a deep-create composition at all. A clean handover states which associations are enabled for create, whether late numbering is in play, and what the expected batch shape from the UI looks like, so the next developer does not have to reverse-engineer it from a failing Fiori Elements save.
Related SAP objects
Reviewed pages this object connects to in the ERPClimb knowledge graph.
Source: ERPClimb — https://erpclimb.com/sap-technical-topics/odata-batch-requests-and-deep-insertsERPClimb is an independent platform and is not affiliated with SAP SE. Reference pages are written and reviewed by SAP consultants for learning and troubleshooting.