ETS-based webhook deduplication with sliding expiry window.
Plaid may deliver the same webhook more than once (at-least-once delivery semantics). This module maintains a sliding window of seen webhook IDs to detect and discard duplicates.
Architecture
Uses a GenServer to own the ETS table and run periodic cleanup. The fast path (check + record) is a single ETS operation — no GenServer call required for normal traffic.
Webhook ID derivation
Plaid does not include a stable unique ID in all webhook payloads. IDs are derived from:
webhook_type+webhook_code+item_id+ quantized timestamp (5s window)
This means the same event delivered twice within 5 seconds is deduplicated. Events delivered more than 5 seconds apart are treated as distinct (edge case — rare in practice).
Window size
Default: 1 hour. Configurable via :webhook_dedup_window_seconds.
For high-volume deployments, reduce this if ETS memory is a concern.
Summary
Functions
Checks if a webhook has been seen before.
Returns a specification to start this module under a supervisor.
Derives a deduplication ID from webhook event fields.
Functions
@spec check_and_record(term()) :: :ok | {:error, :duplicate}
Checks if a webhook has been seen before.
If not seen, records it and returns :ok.
If already seen, returns {:error, :duplicate}.
This is an atomic check-and-set using ETS insert_new/2.
Returns a specification to start this module under a supervisor.
See Supervisor.
Derives a deduplication ID from webhook event fields.
@spec start_link(keyword()) :: GenServer.on_start()