The Effect API + Entity-Cache-First Preload Pattern — Production Default For DeFi Indexers
From: Kaustubh Agrawal — Growth Engineer candidate Companion docs: ENVIO_VERTICAL_PLAYBOOK.md · ENVIO_INDEXER_TEARDOWN.md · ENVIO_ONBOARDING_FORENSIC.md · ENVIO_PAIN_MAP_MATRIX.md
This is a pattern reference, not a finished tutorial. The pattern is real and load-bearing in production at Sablier and Velodrome+Aerodrome — it is documented in Envio's docs, but only as an advanced footnote on the first-30-minutes path. The Growth-Engineering play is promoting it to canonical production default so the next customer inherits it instead of rediscovering it.
Live reference code:
pow/envio-defi-template-v1/src/Effects/TokenMetadata.ts(the Effect definition) andpow/envio-defi-template-v1/src/EventHandlers/PoolFactory.ts(thePromise.all-of-entity-and-Effect-reads at the top of the handler). The PM template has the same pattern atpow/envio-pm-template-v1/src/Effects/OracleRead.tsandpow/envio-pm-template-v1/src/EventHandlers/Settlement.ts.
§1 Why This Pattern Exists
A naive Envio handler reads entities serially: load entity A, await, load entity B, await, mutate, store. Under production event volume on a multi-chain DeFi indexer, this serial RPC + entity-load shape is the dominant latency contributor. Velodrome+Aerodrome's src/PriceOracle.ts and Sablier's envio/streams/mappings/lockup/common/preload.ts both replace this with a cache-first, parallel preload at the top of every handler — entities and external reads dispatched in one go, awaited as a batch, mutated in memory, stored at the end.
This pattern closes two §2 cells:
- Activation × Tech (DeFi) — production-shape patterns absent from the first-30-minutes path. (ENVIO_ONBOARDING_FORENSIC.md: "production patterns documented but not on first-30-minutes path".)
- Retention × Tech (DeFi) — at scale, the absence of this pattern shows up as handlers running twice (re-fetching entities), cache file balloon (no Effect-result deduplication), and slow throughput on multi-event handlers.
The funnel-velocity claim: documenting this as the production default turns a month-2 friction discovery into a day-1 default — meaningfully shifting net-new activation conversion and reducing the in-production friction that triggers tier-up conversations to be reactive rather than proactive.
§2 Anatomy
The pattern has four moving parts. Each is visible in customer repos cited below; the doc here describes their interaction, not their re-implementation.
2.1 The preload function
A handler exports two halves: a loaderPreload that declares everything the handler will need, and a handler that runs the mutation. The loader runs first and returns a typed payload of entities and Effect results; the handler receives that payload and is purely synchronous over already-loaded data.
Customer repo references:
- Sablier:
envio/streams/mappings/lockup/common/preload.tsshows the canonical shape — declarative, sortable by entity, noawaitinside business logic. - Velodrome:
src/PriceOracle.tsuses the same shape for the priciest reads (oracle prices + token metadata).
2.2 Effect API for external reads
Anything outside the indexer's storage — token metadata, forex rates, Coingecko prices, Pyth oracle reads — is wrapped in an Effect. Effects deduplicate identical reads inside a batch and cache results across batches, written to a cache file on disk.
Customer repo references:
- Sablier:
envio/common/effects/token-metadata.tsdefines the canonical token-metadata Effect; reused across all three indexers in their monorepo. - Velodrome:
src/Effects/directory shows Effect-shaped wrappers for oracle reads + token reads.
2.3 Entity-cache-first preload
Inside the loader, entity reads are dispatched in parallel using a cache-first strategy: if the entity is in the in-memory cache for this batch, return immediately; otherwise dispatch a parallel storage read. The storage layer batches the reads underneath. Net effect: a handler that needs 10 entities runs in roughly the time of a single entity read, not 10×.
Customer repo references:
- Velodrome: the Aggregators layer (
src/Aggregators/LiquidityPoolAggregator.ts, ~24KB) is the production reference for cache-first entity preload at scale.
2.4 Cache-size optimisation
Effect-result caches grow without bound by default. Sablier's 0 alias trick encodes "unknown token metadata" as a single byte alias (0) instead of a full token-metadata blob, keeping the on-disk cache file size manageable across the 27-deployment surface. (ENVIO_INDEXER_TEARDOWN.md)
Customer repo references:
- Sablier: the
0alias convention is visible in their effect-cache layer; the implementation pattern is the documentation hook.
§3 Production Guardrails
The pattern prevents three failure modes documented in the indexer teardown:
- Handlers running twice. The serial-await shape inside a handler can cause a re-execution if the runtime detects an unresolved entity dependency mid-flight. The preload-first shape eliminates this — every dependency is resolved before mutation begins.
- Cache file balloon. Without the
0-alias-style optimisation, Effect caches grow indefinitely, eventually causing slow startup on indexer redeploy. The pattern documents the optimisation as part of the default, not as a debugging step. - Multi-chain config sprawl. The pattern composes with the codegen pipelines used by Sablier and Velodrome — preload functions are codegen-friendly because they're declarative, so a 12-chain indexer doesn't require 12 hand-written preload functions.
§4 Walkthrough Hooks (Not Full Code)
A productised version of this pattern should ship as:
- A docs page that lives at the top of the production-patterns section, not buried under "advanced." The page has the shape: 3-paragraph framing → annotated reference to the Sablier preload file (cited, not copy-pasted) → annotated reference to the Velodrome PriceOracle file → guardrails section.
- A Loom walkthrough (10–12 min) where the presenter forks Velodrome's repo, walks through
src/PriceOracle.ts+src/Aggregators/LiquidityPoolAggregator.ts, shows the preload shape, then shows the equivalent in the Sablier repo. Pause-points: each anatomy sub-section above. - A forkable starter that ships only the preload pattern (not a full DeFi indexer) — minimum viable shape, two events, two entities, Effect API for one external read. Lives at a
envio-effect-api-patternrepo slug. (This is a candidate-time-permitting deliverable, hedged.)
§5 Adoption Pathway
The artifact succeeds when:
- The docs page becomes the canonical link Envio team members share when a customer asks "how do I make my handler faster?"
- The DeFi 60-min template (ENVIO_DEFI_60MIN_TEMPLATE.md) cites it as the default — not as an upgrade.
- New DeFi customers ship the pattern from day one, not month two.
The conversion event is qualitative for #1 (link share frequency in support), measurable for #2 (template uses the pattern) and #3 (new-customer indexer code review shows the pattern present).
§6 What This Doc Doesn't Cover
ClickHouse Sink, risk-dashboard query architecture, liquidation handlers — all out of scope. The Effect API pattern is the pre-Postgres optimization; the analytics-tier patterns are downstream. See ENVIO_RISK_DASHBOARD_QUERY_ARCHITECTURE.md for the money-market analytics layer and ENVIO_LIQUIDATION_HANDLER_REFERENCE.md for the trickiest event in that vertical.
Cell: Activation × Tech [DeFi]; Retention × Tech [DeFi] Revenue mechanism: Net-new acquisition (Activation) + tier-up to Dedicated (Retention enables tier-up by removing the friction that triggers reactive churn) Named accounts: Sablier (preload + 0-alias caching), Velodrome+Aerodrome (PriceOracle + Aggregators), LI.FI, Beefy Sibling artifacts: ENVIO_PAIN_MAP_MATRIX.md, ENVIO_DEFI_60MIN_TEMPLATE.md, ENVIO_DEFI_TECH_DIAGNOSTIC.md
— Kaustubh