Every email requires a direct call with a template name
sendEmail("payment-failed-pro-plan-de", user) — you are encoding routing logic in your function call.
Instead of calling sendEmail("payment-failed-template", user) for every case, you fire an event. else.events matches rules, picks the template and delivers.
sendEmail("payment-failed-pro-plan-de", user) — you are encoding routing logic in your function call.
Supporting a new language or plan tier means finding every sendEmail() in your codebase and branching.
Your billing service should not know which template handles German Pro-plan payment failures. That is routing logic, not billing logic.
Multiple send() call sites make it impossible to see all emails a user received from a single place.
An event-based email API inverts the model. Your app describes what happened. else.events figures out what to send.
POST invoice.payment_failed once. Rules and templates handle every plan, locale and tenant variant.
Your billing service does not know which template fires. It just knows a payment failed and fires the event.
Add a new plan tier or locale? Add a rule and a template in the dashboard. Zero code change.
Every event, every matched rule, every email delivery — logged together with domain context.
| Capability | Event-based (else.events) | sendEmail() pattern |
|---|---|---|
| Adding a locale variant | Add a rule + template in dashboard | Find every call site, add branch |
| New plan tier | Add a rule in dashboard | New conditional in application code |
| Routing logic location | Platform rules, visible to all | Buried in application code |
| Audit trail | Centralised per event | Scattered across services |
| Template change without deploy | Yes | No |
The event-based model reduces coupling between your domain logic and your email delivery infrastructure.
Fire your first domain event. Let else.events handle routing, rendering and delivery.