Krizaka
All building blocks

krizaka-notifications

Notifications

E-mail, SMS and webhooks behind one event: applications never talk to an SMTP server again.

The problem it removes

An e-mail sent inside a transaction that rolls back can't be unsent.

Each service that sends its own e-mails embeds an SMTP client, templates and retries. A provider outage becomes a bug in every one of them, and an e-mail sent inside a transaction that then rolls back cannot be unsent.

  • A sign-up that fails because the mail server is slow.
  • A welcome e-mail for an account whose creation rolled back.
  • The same redelivered message sending the same e-mail twice.

What it does

  • Publish evt.notification.requested in your transaction — no SMTP client in your service.
  • Plain-text templates per locale, en as the fallback.
  • E-mail (SMTP), SMS (Twilio) or a webhook to allow-listed hosts: the same request.

A service that consumes evt.notification.requested (and the users events), renders a template for the locale and delivers on the requested channel: SMTP, Twilio SMS, or an HTTP webhook.

The pigeon delivers each message once — or fails where you can see it. A channel that is not configured fails and goes to the dead-letter queue; it is never skipped in silence.

The pigeon's rule

Decisions and trade-offs

  1. We chose

    An event, published through the producer's outbox.

    We refused

    A synchronous "send e-mail" HTTP endpoint.

    Because

    No e-mail for a rolled-back change; an SMTP outage delays mail instead of failing sign-ups; a redelivery is sent once (deduplicated by messageId).

    What it costs you

    RabbitMQ between your application and the service.

  2. We chose

    Plain-text templates on disk, one file per template and locale, an en fallback, a directory you can point elsewhere without a rebuild.

    We refused

    An HTML template engine, for now.

    Because

    A template a non-developer can edit, and nothing to render that can break.

    What it costs you

    Plain text only today.

  3. We chose

    Webhooks only to hosts on an allow-list (empty = no webhooks).

    We refused

    Any URL a requester sends.

    Because

    An open webhook is a server-side request forgery waiting to happen.

    What it costs you

    You declare the hosts.

In code

javaOrderService.java
@Transactional
public Order confirm(Order order) {
  Order confirmed = orders.save(order.confirmed());
  // Not an SMTP call: an event in the same transaction. The service renders and delivers it.
  events.publish(NotificationRouting.NOTIFICATION_REQUESTED, 1,
      new NotificationRequest(Channel.EMAIL, confirmed.email(), "order-confirmed", "fr-FR",
          Map.of("order", confirmed.number())));
  return confirmed;
}

// templates/order-confirmed/fr.txt — first line "Subject: …", then {{order}}; "en" is the fallback.
// SMS (Twilio) and WEBHOOK (allow-listed hosts) are the same request with another Channel.

Don't use it when

  • You need open and click tracking, campaigns or rich HTML e-mails: use a marketing e-mail service.
  • Your application has no message broker and won't run one.
  • You need mobile push: not a channel yet (Orochia uses Expo's push service on its own).

Where it stands

0.1.0 on Maven Central (the -api contract). The service is built from source or as an image; 0.2.0 ships in November.

Published

In progress

  • BOM 0.2.0 manages this block at an unpublished 0.2.0: declare 0.1.0 explicitly until BOM 0.3.0.krizaka-build#11

Adopt it

xmlpom.xml
<dependency>
  <groupId>com.krizaka</groupId>
  <artifactId>krizaka-notifications-api</artifactId>
  <version>0.1.0</version>
</dependency>
<!-- the service itself: ./mvnw -pl krizaka-notifications-service -am spring-boot:run (port 8097) -->

Tell us where it hurts.

A block is right when it survives your code base, not ours. Ask in the block's thread, propose a change as an idea, or report a bug on its repository — every decision on this page is open to a better argument.

The other blocks