Services are starting to send mail. api needs invitation emails now, and the obvious
shape, a typed gateway per service over a vendor SDK, gives every service its own timeout,
its own retry opinion and its own coupling to Resend.
This is the kit.clients case rather than the database-helpers case. It is not an
abstraction over a domain, it is delivery policy plus a provider swap, and both are wrong
in the same way in every service that writes them alone. The README's rule ("code moves in
on its third copy") is about domain abstractions; the carve-out already written down for
kit.clients is the precedent, and the boundary that keeps it honest is that templates,
rendering and "who gets which email" stay in the service.
Proposed surface:
EmailMessage: recipients, subject, text body, html body, sender, reply-to. Nothing else,
because everything else is where providers differ.
Delivery: what the provider said it accepted.
Emailer: a Protocol, so adapters never import it and services fake it structurally.
ResendEmailer: the one adapter, built on a kit.clients.ServiceClient, so it inherits
the deadline, the jittered backoff, the breaker, trace context and the /readyz line
rather than reimplementing them.
No new dependency: Resend is spoken to over the httpx that is already here, never the
vendor SDK.
Services are starting to send mail.
apineeds invitation emails now, and the obviousshape, a typed gateway per service over a vendor SDK, gives every service its own timeout,
its own retry opinion and its own coupling to Resend.
This is the
kit.clientscase rather than the database-helpers case. It is not anabstraction over a domain, it is delivery policy plus a provider swap, and both are wrong
in the same way in every service that writes them alone. The README's rule ("code moves in
on its third copy") is about domain abstractions; the carve-out already written down for
kit.clientsis the precedent, and the boundary that keeps it honest is that templates,rendering and "who gets which email" stay in the service.
Proposed surface:
EmailMessage: recipients, subject, text body, html body, sender, reply-to. Nothing else,because everything else is where providers differ.
Delivery: what the provider said it accepted.Emailer: a Protocol, so adapters never import it and services fake it structurally.ResendEmailer: the one adapter, built on akit.clients.ServiceClient, so it inheritsthe deadline, the jittered backoff, the breaker, trace context and the
/readyzlinerather than reimplementing them.
No new dependency: Resend is spoken to over the
httpxthat is already here, never thevendor SDK.