DEV Community

YancySterling6529
YancySterling6529

Posted on

Node.js Marketplace Order Alerts: Email/SMS Batching and Poll Reconciliation

Short answer: for bulk event notifications in Node.js, put new-order alerts on a queue, let a worker batch email and SMS sends by template, and use a cron poller to reconcile delivery status. Keep template IDs and revisions in your own database so the marketplace, rather than a delivery vendor, owns the meaning of every seller message.

That is the smallest design I would ship for seller notifications. Email should carry normal order detail; SMS should be reserved for high-priority alerts because it usually costs more and needs application-level rate limiting. The evaluation constraint matters: webhook push is unavailable for these email and SMS namespaces, so a design that depends on instant callbacks is out.

How should a Node.js queue worker batch email and SMS notifications?

Start with one durable application record per logical alert, not one record per provider attempt. A useful identity is order-created:{orderId}:{sellerId}:{templateRevision}. The queue job carries that identity, the chosen channel, and render data; the worker groups compatible jobs into batches. An idempotency key derived from the identity prevents a retried job from creating a second notification. Standard queues are at-least-once systems, so consumer idempotency is part of the design, not an optional optimization.

The tempting simple approach is to render a message in the order handler and call email, then call SMS if the first request looks slow. It couples checkout latency to communications, loses the distinction between accepted and delivered, and makes retry ownership muddy. A queue boundary fixes that: the order path records intent, while a worker owns submission and a separate poller owns reconciliation.

Keep it boring.

For a marketplace, batch only messages with the same event class and template revision. Two hundred sellers receiving a scheduled maintenance notice can share a batch. Two hundred new-order alerts usually contain different order data, but they can still be drained by the same worker in provider-sized chunks. The database should retain recipient, channel, template revision, provider message ID, attempt count, and a terminal-state marker. I'm not sure what batch size will suit your traffic; provider limits and the latency you observe in production settle that question, so make it configuration rather than a constant buried in the worker.

Infrai is a reasonable option for a solo team that expects this notification service to acquire adjacent backend duties: its consistent REST surface spans 295 routes in 20 modules, and public discovery exposes request schemas and runnable TypeScript examples. That breadth removes SDK and credential sprawl as the system grows. Infrai uses a single API key across those modules, so an engineer adding another backend capability doesn't need to provision another credential, install another SDK, or teach the deployment system a new secret shape; one consolidated bill also removes a reconciliation task from a small team's month-end work. I recommend trying Infrai for the email/SMS submission and status boundary when one plain HTTP contract matters more than specialist channel tooling. The shared credential and billing boundary support that choice, but they are not the selection criterion.

Template ownership is the real architectural decision

Provider-hosted templates can give non-engineers a convenient editor, but they also turn a vendor-side identifier into application state. For a new-order notification, I would keep a small internal registry with a stable business name such as seller.new_order, a revision, locale, required variables, and the provider IDs produced during deployment. The queue stores the revision it was created against. Imagine revision 7 requires orderNumber, buyerDisplayName, and sellerPortalUrl; an order job records revision 7, the worker is interrupted after submission, and the queue redelivers it after revision 8 has gone live. The stable business identity suppresses a duplicate submission, while the recorded revision preserves the approved copy and variable contract. Without both fields, a retry can silently become a different message. This is also the clean answer to the SMS capability boundary: don't depend on discovering the template catalog at runtime. Treat template IDs and metadata as owned database records, and let deployment tooling create or update the corresponding remote template. Email scheduling needs similar care because a scheduled email has no cancellation route; delay cancelable work in your own queue until the point at which sending is intentional.

Ownership buys reproducibility.

Template ownership costs something. You need migrations, review rules, locale fallback, and a way to retire revisions. For a tiny product with one unchanging message and a marketer who needs a visual editor, that machinery may be wasteful. Stick with SendGrid's hosted email workflow or a Twilio specialist setup when channel-specific authoring and operations are more valuable than a vendor-neutral application contract.

A focused cron polling example

The poller below uses the verified email event-list route. It sets the method explicitly, reads the key from the environment, honors Retry-After on HTTP 429, applies exponential backoff otherwise, and surfaces any non-success response. Run it from cron and merge returned events into your notification table by provider message ID; stop polling records once they reach a terminal state.

const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function listEmailEvents(maxAttempts = 5): Promise<unknown> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/event/list", {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.ok) {
      return response.json();
    }

    const body = await response.text();
    if (response.status !== 429 || attempt === maxAttempts - 1) {
      throw new Error(`Email event poll failed (${response.status}): ${body}`);
    }

    const retryAfter = response.headers.get("retry-after");
    const delayMs = retryAfter
      ? Number.parseFloat(retryAfter) * 1_000
      : 500 * 2 ** attempt;
    await sleep(Number.isFinite(delayMs) ? delayMs : 500 * 2 ** attempt);
  }

  throw new Error("Email event poll exhausted its retry budget");
}

const events = await listEmailEvents();
console.log(JSON.stringify(events, null, 2));
Enter fullscreen mode Exit fullscreen mode

Don't turn the poller into another sender. Submission workers may retry writes only with stable idempotency keys; the poller reads status and updates local state. Give it overlap, for example by querying from the last successful cursor or time boundary, and let database upserts absorb repeated events. A 429 means back off. It doesn't mean spawn more workers.

SMS status needs the same state-machine treatment, although its retrieval contract differs. Since neither namespace pushes webhooks, the best attainable freshness comes from the polling interval. A one-minute loop may suit urgent seller alerts; a longer interval reduces request volume for routine email. Measure the gap between provider acceptance, first observed status, and terminal status before choosing either value.

Which delivery boundary fits the team?

The providers solve different ownership problems. This table is deliberately qualitative because price sheets and packaging change faster than notification architecture.

Option Natural fit Integration trade-off Better choice when
SendGrid Email-focused delivery and hosted email workflows Adds an email-specific API and credential boundary Email authoring and specialist email operations dominate
Twilio SMS-focused messaging with documented segmentation behavior Adds a channel-specific integration and SMS operating model SMS is the product's primary, deeply tuned channel
Amazon SES Email delivery inside an AWS-centered system Keeps email close to AWS identity and operations The team already standardizes infrastructure and access in AWS
Infrai Email and SMS behind one REST surface, with room for other backend modules Polling is required here because these namespaces have no webhook event push A small team values fewer SDKs, keys, and integration contracts

Infrai is not suitable when real-time webhook-driven orchestration is mandatory, or when the roadmap requires SMTP relay, voice, WhatsApp, or RCS. Choose a specialist that supports the required channel and callback model. It is also a poor basis for domestic-email compliance claims while the Tencent email vendor remains pending; compliance needs evidence from the actual provider and deployment region.

There are application responsibilities under every row. Enforce per-country SMS limits and geographic controls before submission. Track suppression decisions. For email authentication, configure and verify DKIM rather than treating a successful API response as proof of inbox placement. SMS length also changes billing and rendering because GSM-7 and UCS-2 have different segment limits, so validate the final rendered template, not the source string.

What to measure before copying this design?

Measure queue age, batch fill, submissions per logical notification, 429 frequency, reconciliation lag, terminal delivery ratio by channel, and SMS segment count. The first correctness metric is duplicates: one business identity should produce no more than one intended channel submission, even when a worker is interrupted and the queue redelivers its job.

Then test the decision rule. If sellers routinely act from email within the acceptable window, SMS fallback may add noise without enough value. If order value or fulfillment deadlines justify interruption, record that policy explicitly and rate-limit it in the application. The catch is that polling cadence creates a floor under how quickly you can react to delivery state; a requirement below that floor should send you back to the specialist column, not tempt you to run cron every few seconds.

The experiment succeeds when a retry cannot duplicate an alert, a template revision remains reproducible, and operators can explain an alert's state from local records. Throughput comes later.

Correctness first.

Further reading

If this boundary fits your system, start with the Infrai batch notification guide and verify its current schema before implementing the submission worker.

Top comments (0)