Lazy deserialization — deserialize event payload on demand #55

Closed
opened 2026-02-21 22:42:29 +00:00 by ash · 0 comments
Owner

Problem

Every loaded event is fully deserialized immediately, even if the handler skips it. Combined with type filtering (#54), this doubles down on wasted work.

Proposal

Return a lazy envelope with metadata available immediately (type, stream, version, timestamp). The Data field is only deserialized when .Data() is called.

type LazyEvent[E any] struct {
    ID, StreamID, EventType string
    Version                 int
    GlobalSequence          uint64
    Timestamp               time.Time
    Metadata                Metadata
    rawBytes                []byte
    resolved                *E
    resolver                func([]byte) (E, error)
}

func (e *LazyEvent[E]) Data() (E, error) {
    if e.resolved != nil { return *e.resolved, nil }
    v, err := e.resolver(e.rawBytes)
    if err != nil { return v, err }
    e.resolved = &v
    return v, nil
}

Benchmark Target

  • Load 1000 events, handler accesses only 100
  • Measure allocs saved from skipping 900 deserializations
  • Compare eager vs lazy path

Considerations

  • This changes the Event type or adds a parallel type — needs careful API design
  • Could be opt-in via LoadLazy() vs Load()
  • Works with both standard ES and DCB
## Problem Every loaded event is fully deserialized immediately, even if the handler skips it. Combined with type filtering (#54), this doubles down on wasted work. ## Proposal Return a lazy envelope with metadata available immediately (type, stream, version, timestamp). The `Data` field is only deserialized when `.Data()` is called. ```go type LazyEvent[E any] struct { ID, StreamID, EventType string Version int GlobalSequence uint64 Timestamp time.Time Metadata Metadata rawBytes []byte resolved *E resolver func([]byte) (E, error) } func (e *LazyEvent[E]) Data() (E, error) { if e.resolved != nil { return *e.resolved, nil } v, err := e.resolver(e.rawBytes) if err != nil { return v, err } e.resolved = &v return v, nil } ``` ## Benchmark Target - Load 1000 events, handler accesses only 100 - Measure allocs saved from skipping 900 deserializations - Compare eager vs lazy path ## Considerations - This changes the Event type or adds a parallel type — needs careful API design - Could be opt-in via `LoadLazy()` vs `Load()` - Works with both standard ES and DCB
ash closed this issue 2026-02-22 14:31:43 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ash/eskit#55
No description provided.