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.requestedin your transaction — no SMTP client in your service. - Plain-text templates per locale,
enas 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.
Decisions and trade-offs
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.
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.
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
@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
<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
- Platform kitAn event published after the commit is lost on the next crash.
- UsersSign-up looks like a weekend. Reset tokens, OAuth and JWTs make it a quarter.
- Billing & creditsDebit after and the work ran on credit nobody had. Debit before and failures get billed.
- Build, BOM & test kitParent POMs drift — and a Spring BOM silently overrides the Boot version you chose.
- Krizaka UIThree products, three token vocabularies, 1,153
light:overrides in one app.