The order's life is already on the wire. Across the OrderHub saga I've been building, the lifecycle facts — OrderPlaced, PaymentProcessed, OrderShipped — are already published to Kafka. Day 41 adds a notification-service that emails (and optionally texts) the customer on each of those events, and the striking part is what it didn't require: not one producer, schema, or existing service was touched. It just subscribes. This is the payoff of everything the event-driven design was for. Here's the build.
A pure consumer, bolted on
The new Maven module (port 8085, Eureka + config wired like the others) is a pure event consumer — it publishes nothing, it only reacts. Three @KafkaListeners in its own consumer group read order-placed, payment-events and shipping-events. That's the whole integration: a new customer-facing capability added to the system without changing a single producer. Because the facts are already on the topics, new consumers are genuinely free — event-driven decoupling doing exactly its job. This took the reactor from 146 to 150 tests, BUILD SUCCESS.
Templated per event
Each event renders through a NotificationTemplates that turns it into a subject and body — an OrderPlaced becomes "Order #ORD-42 placed", a PaymentProcessed · APPROVED becomes "Payment received…", a declined payment gets its own cancellation copy. One place owns the customer-facing wording, so the listeners stay thin and every message is consistent.
A swappable sender abstraction
The service dispatches through a NotificationSender abstraction, not a hard-coded mailer. A real JavaMailSender implementation and a logging fallback sit behind @ConditionalOnProperty, so you can log now and drop in SMTP later with no code change — just config. A Twilio-shaped SmsClient mock covers optional SMS the same way. Crucially, the email/SMS are mock/log in the build, so there's no real SMTP to stand up and the tests stay green without a mail server.
Idempotent on the event id
Kafka is at-least-once: the same event can be redelivered, and a naive consumer would email the customer twice. A NotificationLedger.claim(eventId) makes every dispatch idempotent — the first delivery claims the id and sends; a redelivery of the same id is skipped.
if (!ledger.claim(event.getEventId())) {
log.info("duplicate {}, already notified — skipping", event.getEventId());
return; // exactly-once per event
}
var msg = templates.render(event);
sender.send(customer.email(), msg); // email always
if (smsEnabled) smsClient.send(customer.phone(), msg.smsText()); // SMS optional
At-least-once transport, exactly-once effect — the customer never gets a duplicate.
Dark by default
The whole feature is gated by orderhub.notifications.enabled (default false), bound to each listener's autoStartup. With the flag off, nothing consumes and every prior test stays green — the feature ships dark and one flag turns it on. That's the same pattern every hardened feature here follows: idempotent, behind a swappable transport, off until you flip it.
An @EmbeddedKafka test proves the contract end to end: a consumed event triggers exactly one notification (content asserted against the template) and a redelivery is deduped to zero extra sends. That is the difference between "we send an email somewhere" and a tested guarantee — the assertion pins both the content (it matches the template for that event type) and the count (one send, never two), so a future refactor that breaks either fails the build.
This is also the first genuinely new module the saga has grown — an eighth service — rather than a change to an existing one, and it slotted in without a ripple precisely because it only reads. No producer had to learn that notifications exist.
The lesson of the whole saga lands in this one module. Because the order's lifecycle was published as events rather than buried in a service's private method calls, adding a customer-notification capability meant only listening — no rewiring, no schema change, no risk to the producers. New consumers are free. Emit some events and watch each one get consumed, deduped, templated and sent:
https://dev48v.infy.uk/orderhub/day41-notification-service.html
Top comments (0)