Serialization Adapters: How I Cut Payloads by 94%

7 min readYaseen Khatib · MERN + AI Architect

Rich UI objects are great for rendering and lousy as records. In IntegrateX — a node-graph automation tool — the node you drag around the canvas is packed with measured geometry, hover/selection flags, handle maps, and framework internals. Persist that verbatim and every save ships kilobytes of cached view state, sync gets chatty, and real-time edits run into backpressure. I put a Serialization Adapter at the boundary, kept only the domain fields, and dropped the UI scaffolding. Net: 94% smaller payloads and predictable sync.

Two models, one boundary

The naive approach fuses two things that must stay separate: the render model (what the UI needs to draw) and the transport model (what the wire and database require). The adapter translates both ways, and nothing crosses without it. This is the Data / Serialization Adapter in the pattern I call Trinity Architecture: Presentation renders and dispatches only, a Reactive State / Orchestration layer owns the runtime truth and optimistic updates, and the Adapter shapes lean records for the wire. No layer talks past its neighbor — the UI never formats DB schemas, and the adapter never pokes UI state directly.

adapter.ts
// render model → lean transport record (and back)
export const toRecord = (node: FlowNode): NodeRecord => ({
  id: node.id,
  type: node.type,
  x: Math.round(node.position.x),
  y: Math.round(node.position.y),
  config: node.data.config,   // the only domain data that matters
});

export const fromRecord = (r: NodeRecord): FlowNode =>
  hydrate(r, defaults(r.type));   // recompute view state, don't store it

Derived, never stored

The lever behind the compression is simple: anything that can be derived must not be stored. Dimensions come from layout. Selection is ephemeral. Handle geometry is a function of node type. Persisting them is persisting a cache. In IntegrateX, the orchestrator (Zustand in the middle of my Trinity split) recomputes those bits on load and on change, so the transport stays lean while the UI stays reactive.

The 94%, decomposed

This wasn't a clever codec; it was subtraction. Drop measured geometry, per-node flags, duplicated handles, and framework metadata. Round coordinates to integers. A node that used to serialize to roughly a kilobyte collapses to a few dozen bytes. Multiply by a hundred-node graph, and you reclaim the budget for save, load, and real-time sync — fewer bytes on the wire, fewer renders, less state-synchronization lag.

Most payload-size problems aren't compression problems — they're boundary problems. You're shipping the cache because nobody enforced the line between the view and the record.

For the state-management side of the same project — optimistic updates, sync ordering, and throttled broadcasts — see Compressing the Wire; the system is IntegrateX. I reach for this boundary discipline by default now — the engineer who draws the line between view and record early is the one whose sync layer still behaves at a hundred nodes.

Need an engineer who can build this?

I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.