Reading data with OData is the easy part — GET an entity set, maybe add a $filter, done. Writing data is where V4 shows how much it improved over V2. Three operations cover nearly every write scenario you will meet: PATCH for partial updates, deep insert for creating graphs of related entities, and $batch with change sets for atomic multi-operation writes.
PATCH: partial updates done right
In OData V2, partial updates used MERGE — a method that was never part of the HTTP standard and confused every proxy and firewall it met. V4 replaced it with PATCH, a proper HTTP method with clear semantics: send only the properties you want to change.
PATCH /odata/v4/sales/Orders('450001')
Content-Type: application/json
{
"status": "SHIPPED",
"shippedAt": "2026-09-29T10:30:00Z"
}
Only status and shippedAt change. Every other property keeps its current value. Contrast this with PUT, which in V4 means full replacement — any property you omit gets reset to its default. Mixing those two up is one of the most common OData bugs, and it is worth stating plainly: PATCH for partial, PUT for full replacement.
The response to a successful PATCH is 204 No Content by default (or 200 OK with the updated entity if the client asked for it via the Prefer header — more on that in the V2-vs-V4 post).
PATCH sends only what changed. PUT replaces the whole entity. Choosing the wrong one either silently wipes fields or needlessly ships the entire payload.
Deep insert: creating an entity graph in one request
Suppose you need to create an order along with its line items. The naive approach is N+1 requests: one POST for the order, then one POST per item into the navigation property. Deep insert collapses that into a single POST by nesting the related entities in the payload:
POST /odata/v4/sales/Orders
Content-Type: application/json
{
"customerId": "1001",
"status": "OPEN",
"items": [
{ "productId": "P-100", "quantity": 2, "price": "25.00" },
{ "productId": "P-205", "quantity": 1, "price": "99.00" }
]
}
The service creates the order and both items atomically and returns 201 Created with the full graph (including generated keys). The nested property name (items here) is the navigation property from the metadata.
Deep insert only works for navigation properties that the service allows to be created inline — check $metadata for the navigation property's capabilities. When it is supported, it eliminates an entire category of "half-created" data problems, because the client never holds a partially built graph.
$batch: atomic writes with change sets
Deep insert handles one entity graph. But what about updating three unrelated orders, or deleting five line items, as a single atomic unit? That is what $batch is for.
A batch request is a multipart/mixed payload containing individual HTTP requests. Requests grouped inside a change set execute atomically — all succeed, or all roll back:
POST /odata/v4/sales/$batch
Content-Type: multipart/mixed; boundary=batch_123
--batch_123
Content-Type: multipart/mixed; boundary=changeset_1
--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary
PATCH /odata/v4/sales/Orders('450001')
Content-Type: application/json
{"status": "SHIPPED"}
--changeset_1
Content-Type: application/http
Content-Transfer-Encoding: binary
PATCH /odata/v4/sales/Orders('450002')
Content-Type: application/json
{"status": "SHIPPED"}
--changeset_1--
--batch_123--
Both orders ship, or neither does. Requests outside a change set (directly in the batch body) run independently — useful for bundling reads, where atomicity does not matter and you just want fewer round trips.
A practical note: change sets cannot reference entities created earlier in the same batch via the $1 content-ID syntax in every implementation — support varies. If your scenario needs "create the order, then patch it in the same batch," test it against your specific service before relying on it. When in doubt, deep insert covers the create-graph case more portably.
Use deep insert for creating related entities together. Use $batch change sets when independent write operations must succeed or fail as one unit.
Choosing between the three
| Scenario | Use |
|---|---|
| Change a few fields on one entity | PATCH |
| Create an entity with its children | Deep insert (single POST) |
| Multiple unrelated writes, all-or-nothing | $batch with a change set |
| Bundle independent reads | $batch without a change set |
Master these three and you have covered the write side of OData V4. The read side — query options like $count and $filter functions — deserves its own post, which is exactly what comes next.
Bottom line
PATCH for surgical field updates, deep insert for creating whole object graphs in one round trip, and $batch change sets when independent writes must live or die together. Reach for the lightest option that fits — most write scenarios need nothing more than a well-placed PATCH — and reserve batch payloads for the cases where atomicity genuinely matters. Your payloads stay small, your data stays consistent, and your error handling stays simple.