Learning pathsPricingChecking sign-in…
← LLD problem library

LLD-012 · DESIGN PRACTICE

Notification Dispatcher

Separate intent, preferences, provider attempts and delivery evidence under retries and reordered receipts.

Local Java 21 reference verified: 26 checks. Learner drafts are not executed. Author self-review; independent review pending.

Existing introductory lesson · Version 1

Understand the problem

Design a local notification coordinator with channel adapters, versioned plain-text templates, preference checks, bounded retries and delivery receipts. Explain what accepted, delivered, unknown and cancelled mean, and why provider failover after a timeout can create duplicates.

Before you start

  • Adapter and dependency injection
  • Idempotent command identity
  • State machines and retry backoff
  • External outcome ambiguity

Scope and explicit decisions

  • A local in-memory coordinator for EMAIL/SMS/PUSH provider ports. Tests use synthetic recipients and fake providers; no real messages are sent. Recipient IDs are opaque and address lookup/authentication is an adapter responsibility.
  • Templates are plain text with named {field} placeholders. All referenced fields must exist and unused supplied fields reject. Values use literal replacement; HTML escaping, localization, attachment handling and provider payload limits are not implemented.
  • Template version, text and input fields are part of submit identity; rendered body is frozen in Delivery. A production registry must prevent reusing a version label for different semantics; this reference stores the actual accepted body so old deliveries do not change.
  • Preferences are default-deny and checked immediately before a QUEUED delivery is claimed. Opt-out after the provider call starts cannot retract that in-flight effect. Quiet hours and multi-channel fallback policy are extensions.
  • dispatch claims SENDING under a monitor, invokes the provider outside it, then conditionally accepts the result. One delivery ID is the provider idempotency key across retry attempts; provider support for that key is assumed by the adapter contract, not guaranteed by this class.
  • RETRYABLE means a definite retryable no-acceptance result. Bounded exponential backoff is deterministic without jitter in this reference; caller workers must trigger due dispatches. UNKNOWN or exceptions do not auto-retry or fail over.
  • ACCEPTED means the provider accepted the send; DELIVERED requires a trusted correlated receipt. Webhooks can finish during the call; a later synchronous ACCEPTED must not regress DELIVERED. Late failure receipts cannot regress DELIVERED.
  • receipt is a trusted adapter API using an internal delivery correlation; it does not authenticate provider webhooks or validate provider signatures itself. Delivered may override a prior failure as new authoritative evidence. Receipts for QUEUED/CANCELLED deliveries reject and need reconciliation if they contradict the declared no-acceptance contract.
  • cancel supports QUEUED only (and repeated CANCELLED); it cannot unsend SENDING/UNKNOWN/ACCEPTED. Attempt/history/dedupe records are unbounded in memory; there is no durable queue, outbox, background scheduler or restart recovery.

Worked examples

Input: QUEUED v1 body, allowed email, retryable result at t=0 with baseDelay10.

Outcome: Requeued for t=10 using the same delivery ID. t=9 rejects; t=10 may become ACCEPTED.

Retry schedule, attempt identity and delivery status are distinct concerns.

Input: A delivery receipt arrives while send is still in flight, then send returns ACCEPTED.

Outcome: The record stays DELIVERED.

Outcome precedence protects stronger evidence from response reordering.

Your design attempt

Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.

Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.

Solution.java
Run program · Java 21

No unsaved changes · 208/20000

Self-review

Checks apply to this visit. Self-review is not a passed submission or verified mastery.

Try before revealing

Hint 1

Which event proves acceptance, and which proves delivery?

Hint 2

What identity survives a retry, and when are preferences authoritative?

Hint 3

How can a webhook arrive before send returns without corrupting status?

Requirements and invariants

R1 — Submit retries preserve one immutable delivery identity/body. submit protects I1.

R2 — Only one dispatch owns the active SENDING attempt; provider I/O is outside the monitor. dispatch protects I2.

R3 — DELIVERED cannot regress due to a late weaker result. receipt protects I3.

R4 — Ineligible recipients are cancelled before provider invocation. setAllowed protects I2.

I1: A submit key maps one structured request fingerprint to a stable Delivery and frozen body.

I2: Only a due QUEUED record passing current preference check can become SENDING, and one caller owns that transition.

I3: Trusted DELIVERED evidence is terminal against later weaker acceptance/failure; receipt IDs reject conflicting reuse.

Derive the design

A synchronous notifier needs more than a channel switch

A simple notifier can choose email or SMS and call a provider, but one success boolean conflates provider acceptance and actual delivery. The refined design stores immutable intent, isolates channel details behind Provider, claims one attempt and treats receipts as separate evidence. It preserves a stable delivery identity through retry instead of recreating intent each time.

Alternative: An event/outbox plus durable worker is appropriate for production fan-out, but those components do not define business semantics automatically. You still need preference-check timing, provider-result categories, duplicate identity and unknown-outcome policy.

Define acknowledgement vocabulary first

Accepted, delivered, read, failed and unknown are not interchangeable. The reference supports accepted/delivered and makes unknown nonretryable until evidence arrives.

Keep payload and command identity stable

Render an immutable body once; fingerprint actual template and fields. This allows a retry key mismatch to reveal accidental intent changes rather than send another message.

Claim locally, call remotely, then compare state

The synchronized claim prevents two workers calling for one due record. Provider I/O happens outside ownership; final conditional update avoids overwriting a receipt that completed meanwhile.

Make cancellation timing honest

Before send, current preferences can cancel work. After a remote side effect starts, local cancellation cannot guarantee retraction. Keep unsupported receipt contradictions visible for reconciliation.

Responsibilities and ownership

Dispatcher

Owns: Intent identity, preferences, attempt counts, due times and receipt outcomes

Methods: submit, dispatch, receipt, cancel

Serializes local state, not the remote provider side effect.

Provider

Owns: Channel-specific send mapping and remote idempotency contract

Methods: send(delivery,idempotencyKey)

Returns normalized ACCEPTED/RETRYABLE/PERMANENT/UNKNOWN; performs no domain-state mutation directly.

Template / render

Owns: Immutable text/version and strict field substitution

Methods: render

Plain text only; not a safe HTML templating engine.

Delivery / Receipt

Owns: Frozen recipient/channel/body and evidence identity

Methods: Immutable records

A state snapshot is not a proof of external exactly-once delivery.

Dispatch ownership boundary

Tests use fake providers only. This is a local component, not a live messaging service.

  1. Immutable intent + preference
  2. ↓ Default deny and stable key

    Claim due QUEUED→SENDING
  3. ↓ One local owner

    Provider call outside monitor
  4. ↓ Preserve stronger concurrent evidence

    Conditional result / trusted receipt
Provider outcome handling

Accepted and delivered remain distinct. Permanent failures stop ordinary retry.

Normalized provider result

Same delivery key on retry

Definite retryable: backoff within cap

Do not blindly retry or fail over

Unknown: hold for reconciliation

Data model, schema and relationships

Notification intent, rendered content, recipient preference, channel delivery and provider attempt are distinct entities. Provider accepted and user delivered are different evidence states.

Implementation boundary: The reference implements strict plain-text templates, default-deny preferences and simulated providers. Durable tables, authenticated receipts and real delivery are extensions.

These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.

recipient: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

recipient relationshipsrecipient 1 to preference 0..*: controls channels. preference.recipient_id → recipient.id. recipient 1 to delivery 0..*: receives. delivery.recipient_id → recipient.id.controls channels1 parent · 0..* childrenpreferencerecipient_id + channel [PK]enabledreceives1 parent · 0..* childrendeliveryid [PK]notification_id [FK]recipient_id [FK]recipientid [PK]address
template: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

template relationshipstemplate 1 to notification 0..*: renders intent. notification.template_id → template.id.renders intent1 parent · 0..* childrennotificationid [PK]template_id [FK]payloadtemplateid [PK]body
notification: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

notification relationshipsnotification 1 to delivery 0..*: targets. delivery.notification_id → notification.id.targets1 parent · 0..* childrendeliveryid [PK]notification_id [FK]recipient_id [FK]notificationid [PK]template_id [FK]payload
delivery: relationship map

Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.

delivery relationshipsdelivery 1 to send_attempt 0..*: attempts send. send_attempt.delivery_id → delivery.id.attempts send1 parent · 0..* childrensend_attemptid [PK]delivery_id [FK]external_keydeliveryid [PK]notification_id [FK]recipient_id [FK]

Entity definitions and field contracts

recipient

Stable destination owner.

FieldType / keyMeaning and reference
iduuid · PKRecipient identity
addresstext · VALUEValidated channel address

Code mapping: Recipient identity/input.

  • Address visibility/access and verification belong to the delivery adapter.

preference

Channel permission by recipient.

FieldType / keyMeaning and reference
recipient_iduuid · PK,FKPreference owner · recipient.id
channelenum · PKEmail SMS etc
enabledboolean · VALUEExplicit permission

Code mapping: Preference map.

  • PK(recipient_id,channel); missing preference means deny.

template

Versioned rendering contract.

FieldType / keyMeaning and reference
idtext · PKTemplate/version identity
bodytext · VALUEStrict plain-text placeholders

Code mapping: Template record.

  • Missing or unexpected variable policy must be explicit.

notification

Accepted immutable intent.

FieldType / keyMeaning and reference
iduuid · PKIntent identity
template_idtext · FKPinned template · template.id
payloadjson · VALUEValidated rendering variables

Code mapping: Notification intent.

  • Stable caller key binds full request meaning.

delivery

One recipient/channel logical message.

FieldType / keyMeaning and reference
iduuid · PKDelivery identity
notification_iduuid · FKIntent · notification.id
recipient_iduuid · FKDestination owner · recipient.id
channelenum · VALUEChosen channel
stateenum · VALUEAccepted delivered failed unknown etc
next_duetimestamp? · VALUERetry eligibility instant after bounded provider policy

Code mapping: Delivery state.

  • Unique(notification_id,recipient_id,channel); apply preference before external send.

send_attempt

Provider request evidence.

FieldType / keyMeaning and reference
iduuid · PKAttempt identity
delivery_iduuid · FKLogical delivery · delivery.id
external_keytext · VALUEStable retry identity
outcomeenum · VALUEKnown or unknown result

Code mapping: Attempt/retry evidence.

  • Attempts reuse logical provider key when provider guarantees dedupe.

Relationship cardinalities and their reasons

Parent → childMultiplicityFK / why
recipient → preference

controls channels

1 parent per child; 0..* children per parentpreference.recipient_id → recipient.id

Missing permission never silently opts a user in.

template → notification

renders intent

1 parent per child; 0..* children per parentnotification.template_id → template.id

Intent pins template meaning to avoid retry changing text.

notification → delivery

targets

1 parent per child; 0..* children per parentdelivery.notification_id → notification.id

One intent expands into explicitly owned deliveries.

recipient → delivery

receives

1 parent per child; 0..* children per parentdelivery.recipient_id → recipient.id

Delivery is scoped to a recipient/channel.

delivery → send_attempt

attempts send

1 parent per child; 0..* children per parentsend_attempt.delivery_id → delivery.id

Attempt count differs from number of logical messages.

Transaction boundaries

  1. Accept: validate template/variables and operation fingerprint; persist logical deliveries and send intent together.
  2. Receipt: authenticate provider evidence outside this core, deduplicate it and apply monotonic state precedence; never regress DELIVERED to a late ACCEPTED.

Indexes and uniqueness

  1. Unique delivery scope and provider message/key identities; index due delivery(state,next_due).
  2. Index notification caller idempotency scope; store request fingerprint and retention contract.

Schema traps and failure cases

  1. Provider failover after UNKNOWN can duplicate delivery unless reconciled/deduped across providers.
  2. Removing preference after send cannot retract a delivered message; define evaluation time.

Trace the records through one operation

  1. Recipient opts into email but has no SMS preference; SMS stays denied.
  2. Email provider returns UNKNOWN; retry/reconcile retains stable delivery identity.
  3. A delivered receipt dominates a later accepted receipt.

Method contracts

submit(key,recipient,channel,Template,fields) → Delivery

Validate configured adapter and strict rendering. Store request fingerprint and immutable QUEUED body; duplicate mismatch rejects.

setAllowed(recipient,channels)

Replace the immutable local preference set. Missing recipients are denied; dispatch rechecks before claim.

dispatch(deliveryId,now) → Delivery

Validate due time/state; claim once and increment attempt. Release monitor for provider I/O; apply normalized result only if still SENDING.

receipt(eventId,deliveryId,delivered) → Delivery

Trusted correlated terminal evidence only after an attempt. Deduplicate event IDs; DELIVERED does not regress to failure. Reject unsupported queued/cancelled timing.

cancel(deliveryId)

Cancel unsent QUEUED work, including a retry waiting after definite no-acceptance. In-flight or unknown outcomes cannot be retracted through this API.

What, when, why and how

Each concept is attached to the requirement it protects, its implementation, an alternative and a test.

Adapter and normalized provider outcome

what
Map different vendor responses into a precise domain result.
when
Email/SMS/push differ in payloads, receipts and failure codes.
why
Domain retry logic should not spread vendor-specific conditionals across the coordinator.
how
Provider returns ACCEPTED, definite RETRYABLE, PERMANENT or UNKNOWN with an optional provider ID; accepted results require a nonblank provider ID. Exceptions become UNKNOWN, with interruption restored.

Code: Provider, ProviderResult, dispatch · Invariants: I2

Alternatives, cost and failure example

Alternative: A dedicated adapter per channel/provider can also own signature validation and status-query behavior. Do not fake unavailable delivery guarantees.

Cost: Normalization can lose important distinctions; preserve raw/provider metadata in a durable implementation for audit.

Counterexample: Map a network timeout to RETRYABLE even though the provider might already have accepted.

Test: Throw from a fake provider and verify UNKNOWN cannot be automatically redispatched.

Immutable rendered intent and preference boundary

what
Freeze what was requested, while checking whether sending is still allowed at the declared moment.
when
Templates or recipient choices change while work is queued.
why
A later template edit must not silently alter old messages, and a queued opt-out must be respected before send.
how
submit renders once and stores body/version. dispatch checks current allowed channels under the same monitor that claims SENDING.

Code: submit, render, setAllowed, dispatch · Invariants: I1, I2

Alternatives, cost and failure example

Alternative: Some products intentionally re-render or re-evaluate channel selection at send time; then snapshot/version that decision and test reproducibility.

Cost: The local preference check cannot retract an already started remote send. Payload/recipient validation still belongs to trusted adapters.

Counterexample: A recipient opts out before dispatch but a stale enqueue-time preference still triggers a call.

Test: Update preferences after submission; assert CANCELLED and unchanged provider call count.

Bounded retry with stable delivery identity

what
Retry one logical delivery only when the prior outcome permits it.
when
Provider gives a definite transient no-acceptance result.
why
Fresh identities can duplicate the message; unbounded retries overload providers and hide permanent failures.
how
Attempt cap is 1..10. Retry time is now+baseDelay×2^(attempt-1) with checked arithmetic; the delivery ID stays the provider key. Unknown is not requeued.

Code: dispatch, Delivery · Invariants: I1, I2

Alternatives, cost and failure example

Alternative: Add injected bounded jitter for fleet behavior; deterministic backoff here makes fixtures exact but is not a recommendation for synchronized production retries.

Cost: No background scheduler exists. Queue storage and due execution are responsibilities of a real worker/outbox extension.

Counterexample: Retry at t=9 when nextAt=10, or retry an unknown result through another provider.

Test: Test due boundary, attempt cap and identical provider key across retryable→accepted calls.

Monotonic evidence under reordered callbacks

what
Stronger terminal delivery evidence cannot be overwritten by a later weaker result.
when
A webhook arrives before the synchronous provider response or a late failure follows delivery.
why
Naïve last-write-wins can turn DELIVERED back into ACCEPTED/FAILED.
how
receipt deduplicates identity and writes DELIVERED; dispatch applies its response only while still SENDING. Later weaker receipts leave delivered unchanged.

Code: receipt, dispatch · Invariants: I3

Alternatives, cost and failure example

Alternative: A richer evidence/event log can retain every provider fact and derive status by a versioned precedence rule rather than discard weaker history.

Cost: Provider-specific semantics and contradictions need audit/review; the local boolean receipt is a deliberately simplified trusted contract.

Counterexample: Webhook sets DELIVERED while send is blocked; send later returns ACCEPTED and overwrites it.

Test: Use latches to deliver the receipt during the provider call and assert final status stays DELIVERED.

Read and run the Java reference

Reveal complete Solution.java

Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.

Solution.java Java 21
import java.util.*;
import java.util.regex.*;
/** Local dispatch coordinator. No real messages are sent by the included tests. */
public final class Solution {
    private Solution() {}
    public enum Channel { EMAIL, SMS, PUSH }
    public enum State { QUEUED, SENDING, ACCEPTED, DELIVERED, FAILED, CANCELLED, UNKNOWN }
    public enum Outcome { ACCEPTED, RETRYABLE, PERMANENT, UNKNOWN }
    public record Template(String version,String text){public Template{if(version==null||version.isBlank()||text==null)throw new IllegalArgumentException("template");}}
    public record Delivery(String id,String recipient,Channel channel,String templateVersion,String body,State state,int attempts,long nextAt,String providerId){}
    public record ProviderResult(Outcome outcome,String providerId){public ProviderResult{Objects.requireNonNull(outcome);if(providerId==null||(outcome==Outcome.ACCEPTED&&providerId.isBlank()))throw new IllegalArgumentException("provider ID");}}
    @FunctionalInterface public interface Provider { ProviderResult send(Delivery delivery,String idempotencyKey) throws Exception; }
    private record Request(String recipient,Channel channel,Template template,Map<String,String> fields){public Request{fields=Map.copyOf(fields);}}
    private record Operation(Request request,String deliveryId){}
    private record Receipt(String deliveryId,boolean delivered){}
    public static String render(Template template,Map<String,String> values){
        Objects.requireNonNull(template);var fields=Map.copyOf(values);var matcher=Pattern.compile("\\{([A-Za-z][A-Za-z0-9_]*)\\}").matcher(template.text());var output=new StringBuilder();var used=new HashSet<String>();
        while(matcher.find()){String name=matcher.group(1),value=fields.get(name);if(value==null)throw new IllegalArgumentException("missing template field");used.add(name);matcher.appendReplacement(output,Matcher.quoteReplacement(value));}matcher.appendTail(output);
        if(!used.equals(fields.keySet()))throw new IllegalArgumentException("unused fields");return output.toString();
    }
    public static final class Dispatcher {
        private final Map<Channel,Provider> providers;
        private final int maxAttempts;
        private final long baseDelay;
        private final Map<String,Set<Channel>> preferences=new HashMap<>();
        private final Map<String,Delivery> deliveries=new LinkedHashMap<>();
        private final Map<String,Operation> operations=new HashMap<>();
        private final Map<String,Receipt> receipts=new HashMap<>();
        private long sequence,time;
        public Dispatcher(Map<Channel,Provider> providers,int maxAttempts,long baseDelay){this.providers=Map.copyOf(providers);if(maxAttempts<1||maxAttempts>10||baseDelay<1||baseDelay>Long.MAX_VALUE/1024)throw new IllegalArgumentException("retry bounds");this.maxAttempts=maxAttempts;this.baseDelay=baseDelay;}
        private void at(long now){if(now<time)throw new IllegalArgumentException("time");time=now;}
        public synchronized void setAllowed(String recipient,Set<Channel> channels){if(recipient==null||recipient.isBlank())throw new IllegalArgumentException("recipient");preferences.put(recipient,Set.copyOf(channels));}
        public synchronized Delivery submit(String key,String recipient,Channel channel,Template template,Map<String,String> fields){
            if(key==null||key.isBlank()||recipient==null||recipient.isBlank()||!providers.containsKey(channel))throw new IllegalArgumentException("request");
            var request=new Request(recipient,channel,template,fields);var old=operations.get(key);if(old!=null){if(!old.request().equals(request))throw new IllegalArgumentException("key reused");return get(old.deliveryId());}
            String body=render(template,fields);var delivery=new Delivery("D"+(++sequence),recipient,channel,template.version(),body,State.QUEUED,0,0,"");deliveries.put(delivery.id(),delivery);operations.put(key,new Operation(request,delivery.id()));return delivery;
        }
        private static Delivery change(Delivery d,State state,int attempts,long next,String providerId){return new Delivery(d.id(),d.recipient(),d.channel(),d.templateVersion(),d.body(),state,attempts,next,providerId);}
        public Delivery dispatch(String id,long now){
            final Delivery claimed;final long retryTime;
            synchronized(this){
                at(now);var d=get(id);if(d.state()!=State.QUEUED||now<d.nextAt())throw new IllegalStateException("not due");
                if(!preferences.getOrDefault(d.recipient(),Set.of()).contains(d.channel())){var cancelled=change(d,State.CANCELLED,d.attempts(),d.nextAt(),d.providerId());deliveries.put(id,cancelled);return cancelled;}
                retryTime=Math.addExact(now,Math.multiplyExact(baseDelay,1L<<d.attempts()));
                claimed=change(d,State.SENDING,d.attempts()+1,d.nextAt(),d.providerId());deliveries.put(id,claimed);
            }
            ProviderResult result;
            try{result=Objects.requireNonNull(providers.get(claimed.channel()).send(claimed,claimed.id()));}
            catch(Exception e){if(e instanceof InterruptedException)Thread.currentThread().interrupt();result=new ProviderResult(Outcome.UNKNOWN,"");}
            synchronized(this){
                var current=get(id);
                // A delivery webhook may have completed while the request was in flight.
                if(current.state()!=State.SENDING)return current;
                State state=switch(result.outcome()){case ACCEPTED->State.ACCEPTED;case RETRYABLE->claimed.attempts()<maxAttempts?State.QUEUED:State.FAILED;case PERMANENT->State.FAILED;case UNKNOWN->State.UNKNOWN;};
                var updated=change(claimed,state,claimed.attempts(),state==State.QUEUED?retryTime:claimed.nextAt(),result.providerId());deliveries.put(id,updated);return updated;
            }
        }
        public synchronized Delivery receipt(String eventId,String deliveryId,boolean delivered){
            if(eventId==null||eventId.isBlank())throw new IllegalArgumentException("event");var old=receipts.get(eventId);var receipt=new Receipt(deliveryId,delivered);
            if(old!=null){if(!old.equals(receipt))throw new IllegalArgumentException("event reused");return get(deliveryId);}
            var d=get(deliveryId);if(!Set.of(State.SENDING,State.ACCEPTED,State.UNKNOWN,State.DELIVERED,State.FAILED).contains(d.state()))throw new IllegalStateException("no provider attempt");
            if(d.attempts()==0)throw new IllegalStateException("no provider attempt");
            receipts.put(eventId,receipt);
            if(d.state()==State.DELIVERED)return d; // A late failure cannot regress delivered.
            var next=change(d,delivered?State.DELIVERED:State.FAILED,d.attempts(),d.nextAt(),d.providerId());deliveries.put(deliveryId,next);return next;
        }
        public synchronized Delivery cancel(String id){var d=get(id);if(d.state()!=State.QUEUED&&d.state()!=State.CANCELLED)throw new IllegalStateException("cannot retract provider attempt");var next=change(d,State.CANCELLED,d.attempts(),d.nextAt(),d.providerId());deliveries.put(id,next);return next;}
        public synchronized Delivery get(String id){var d=deliveries.get(id);if(d==null)throw new IllegalArgumentException("delivery");return d;}
    }
}

render uses literal replacement

Matcher.quoteReplacement prevents field values containing dollar/backslash from becoming replacement syntax. Missing and unused fields reject before a delivery is stored.

dispatch prepares retry arithmetic before send

The next deadline is checked before publishing SENDING. A regular overflow error cannot occur after the external send just because backoff calculation was delayed.

provider result applies conditionally

If a webhook changed state while the call ran, dispatch returns that state instead of imposing its stale response.

receipt protects monotonic delivery

Duplicate event identity resolves current delivery. Conflicting reuse rejects; late false evidence cannot regress a prior DELIVERED record.

Complexity and limits

  • Template rendering is O(T+output+F) for template length T and field map F under the declared placeholder syntax; literal replacement avoids interpreting dollar/backslash as regex substitution.
  • submit/receipt lookups are expected O(1) plus copied request/body fields. dispatch local work is expected O(1), excluding arbitrary provider latency.
  • History, operations, preferences and receipts grow with accepted work; no bounded durable fan-out queue is implemented. Attempts per delivery are bounded, not total retained data.
  • Provider calls are outside the monitor. Shared-state contention remains local; there is no distributed claim/lease and no exactly-once remote guarantee.

Walk through the actual reference

These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.

Retry only a definite retryable result

Step 1 of 3: Submit immutable v1 body

QUEUED with stable delivery ID

Invariant: Retries preserve logical identity

Read the complete trace table
ActionState after actionInvariant
Submit immutable v1 bodyQUEUED with stable delivery IDRetries preserve logical identity
Provider returns RETRYABLE at t=0QUEUED for t=10Backoff and attempt cap enforced
Retry at t=10 then delivery receiptACCEPTED then DELIVEREDProvider acceptance is not delivery

Webhook races synchronous provider response

Step 1 of 3: Claim SENDING and call provider outside monitor

Second dispatcher rejected

Invariant: One current send attempt

Read the complete trace table
ActionState after actionInvariant
Claim SENDING and call provider outside monitorSecond dispatcher rejectedOne current send attempt
Delivery receipt arrives before returnDELIVEREDAuthoritative receipt wins
Provider call returns ACCEPTEDRemains DELIVEREDNo regression to weaker status

Mistakes and counterexamples

Treat provider acceptance as delivery

Failing case: The provider accepts a message that later bounces.

Repair: Use separate accepted and trusted receipt states.

Retry every exception

Failing case: A timeout follows a successful remote acceptance.

Repair: Classify unknown separately and reconcile a stable key.

Check preferences only at enqueue

Failing case: Recipient opts out while queued.

Repair: Recheck at the authoritative send claim and document the remaining in-flight window.

Use last callback wins

Failing case: A late ACCEPTED response replaces DELIVERED.

Repair: Define evidence precedence and conditional updates.

Engineer → Senior → Staff

Choose a deep dive

Separate intent, rendering and channel adapter.

Executed: strict plain-text templates, stable submit identity, default-deny preferences and normalized outcomes.

Change exercise

Add a provider adapter that maps permanent invalid-recipient errors differently from transient throttling.

Worked answer guidance

Keep vendor details in the adapter and preserve domain categories. A timeout is UNKNOWN, not a definite retryable rejection.

Interview follow-ups

Why are unused fields rejected?

The strict contract catches template/parameter mismatches instead of silently ignoring stale inputs. A more permissive policy is possible if explicitly chosen.

Does a template version guarantee immutable semantics?

Not by itself. A registry must enforce that rule; this reference additionally stores actual accepted text/body.

Return to the lesson →

Provenance and scope of evidence

Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.

Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.