Go SCIM batch PATCH worker hitting 422 on membership conflicts during delta sync

The Go worker for SCIM group synchronization is breaking on batch updates. Batching the PATCH requests to /api/v2/scim/v2/Groups/{id} seems right, but GC throws a 422 Unprocessable Entity when two groups in the same batch share a member that’s locked.

Worker traverses the source directory tree, finds changes, and builds the payload via delta calculation using hash comparison.

type BatchUpdate struct {
 GroupID string
 Ops []SCIMOp
}

batch := make([]BatchUpdate, 0)
for _, g := range deltas {
 batch = append(batch, BatchUpdate{
 GroupID: g.ID,
 Ops: []SCIMOp{{
 Op: "replace",
 Path: "members",
 Value: g.Members,
 }},
 })
}

Partial update failure is the blocker. If group A patches fine but group B hits a conflict, the whole batch response comes back failed. Reconciliation report generation needs to track successful group IDs separately. Can’t just swallow the error.

Source directory holds authority, so GC needs to match the source state. GC throws 422 because GC tries to add a member to group B that group A is currently claiming, or something like that. Lock state isn’t clearing fast enough between batch items.

Pre-resolving members blows up the API call count. DataLoader handles this in the GraphQL gateway layer, but this worker is raw HTTP.

  1. Need to isolate failures per group in the batch.
  2. Handle membership conflicts without aborting the run.
  3. Generate the report comparing source vs target structures after the sync.

Any way to force a membership conflict resolution by doing a DELETE on the member path before the replace? Or is there a flag on the PATCH to ignore conflicts?

Hash logic is solid. Just the sync execution is messy.

The 422 is just the platform choking on concurrent writes. Honestly, you’ll need to serialize those updates, and don’t batch members that share a locked state. Check your KEY CONFIGURATIONS first, then drop this payload into your loop:

{
 "Operations": [{ "op": "add", "path": "members", "value": [{ "value": "usr-123" }] }]
}

Run a quick dry run with curl -X PATCH /api/v2/scim/v2/Groups/{id} to verify the lock state before firing off the rest.

genesyscloud-scim chokes with a 422 when multiple patches hit the same locked member ID in a burst. The backend rejects the overlap. You’ve got to deduplicate operations before the batch goes out.

seen := make(map[string]bool)
for _, req := range batch {
 if seen[req.MemberID] { continue }
 seen[req.MemberID] = true
 queue(req)
}
req.Header.Set("If-Match", etag)

SCIM updates bomb when the header is missing. You’ll hit that 422 wall every time the concurrency spikes.

  1. Cache the ETag from the initial GET.
  2. Pass it in the header map.

Header missing. It’s a strict check. The backend won’t budge.