0

Imagin I have two microservices that need to send email to a customer about some important account realted things that happened.

For example, Billing service would like to send an email when a card has expired and Users service when a password was reset.

These two services use the help of another service, let's call it email service, that is responsible to get the data and prepare an email from a predefined template with all the information necessary.

enter image description here

Now, let's take Card expired event as an example. It contains information that is known to Billing service like the card last 4 digits and the expiration date. But the email template also contains some more additional needed data like the user's name and possibly some other data that the neither billing service nor the email services have and is owned by another micro service.

Where do you think fetching all the information needed for the email to be sent should be handled in ? As I see there are two options:

  1. Billing service will prepare all the information needed for the email, including the information from billing service business domain (card expiry and last4 digits) and also anything it needs to fetch from other services
  2. Email service will fetch any additional information it know it needs from other services that was not contained in the event data that came from billing service in the Card expired event.

In my perspective, both options are not great and I can see downsides for each:

With option #1, billing service need to know what the email template contains in order to fetch all the data it needs, which creates a coupling.

With option #2, Email service can become big if for every email type it will have to go and fetch information from other places and it's also not great.

This must be a very common pattern to handle, and I was wondering what is the right approach for it.

Thanks!

1
  • For option 2, email service need not be bloated with full "awareness" of the context. Hello ${user-svc:/customers/:id} is enough to tell it to ping some service API and place the value, when passed an id.
    – S.D.
    Commented Mar 6 at 12:50

3 Answers 3

3

Yes, it is a very common antipattern to handle, which is related to technical domain cuts instead of business domains.

First of all, I'd ask myself, whether sending an email actually warrants a service. What is its business value? What does it actually do on its own?

Then, if the billing service holds the payment data and is responsible to notify the user of an expired card, it should also have the contact data needed to send this information. If the user's address changes, the user service sends a message to all interested parties, which contains the new contact information. The billing serivce updates its own copy, and has everything it needs to create the corresponding email.

This way, the billing service can work, even if the user service is currently down, which was the reason for separating them in the first place, right?

On the other hand, you might argue, that the user service is responsible for user communication. In this case, the billing service should send a message "someone please notify user id #123 that his card is expired", and the message is picked up and processed by the user service.

Which way you choose is up to your business domain, but coupling services the way you suggest, leads to a distributed big ball of mud, and to headaches in the long run.

3
  • Thanks. Given you have a service responsible of billing, would you handle all billing email sending requirements in that service ? What if you have 10 other services that need to send emails ? You would be duplicating a lot of email sending boilerplate. Also, there are things like email templates that might be shared between all services (like common email layout etc) So I am not sure what you are suggesting ?
    – Michael
    Commented Mar 7 at 10:25
  • 1
    Regarding your first question: yes. The billing service should cover every kind of communication which belongs in its business domain. This may also include messages like "dear customer, we charged your credit card with $100." EMail and boilerplate: this is a purely technical decision. When I separate services, I always ask myself, what does that service do, when all other services are down? When the answer is "nothing", then there's no business value in the service. And to reduce boilerplate code, you could use a library.
    – mtj
    Commented Mar 7 at 10:35
  • Note that "then there's no business value" is somewhat generalized, and you may be able to find exceptions. It's just meant as a first indicator.
    – mtj
    Commented Mar 7 at 10:36
2

I would go for option 1. But, I would send a large object including all the customer and order data.

This gives the email service enough data that it can deal with a large range of template fields for a large range off possible emails.

All your email calls can be the same:

emailer.EmailCustomer("paymentRecieved",orderData)
emailer.EmailCustomer("paymentRejected",orderData)
emailer.EmailCustomer("orderPlaced",orderData)

The edge case here is the payment data, which you want to limit sending over the wire as much as possible.

Having a big queue of random emails, some of which contain credit card details doesn't sound like a great idea. But you shouldn't be sending that stuff in an email anyway, so if the last four digits has been OK'ed to send on an email it should be fine on your internal queue.

Option 2 doesn't really help you much with the coupling. If the email service calls out for the data, it has to call something and that thing can only respond with the data it has. Sure you have reversed the dependency, but it hasn't really gained you anything.

4
  • Problem with option 1 is that, imagine you have some piece of data that repeats in every email template and is only available from service A. Now you have duplicated fetching that data from service A from all the services that need to send an email and don't have that piece of information
    – Michael
    Commented Mar 6 at 11:21
  • i mean if you need it in the template you have to get it. I assume you mean, ONE template has some odd data and now everything has to get it even though they don't need it for their email. But if you have a template system, all the fields have to be avaiable to potentially be put in a template.
    – Ewan
    Commented Mar 6 at 12:30
  • Right, my worry is that all of these services have to get data that has nothing to do with their business logic, sometimes even duplicating the act of getting that data if it's common to that template.
    – Michael
    Commented Mar 7 at 8:33
  • its a danger, you might add emailer.EmailSupplier with different objects for example, but, if your domain modeling is good, large groups of services should be working with the same set of models. All the emails relating to the ordering/signup process etc you would expect to work with EmailCustomer
    – Ewan
    Commented Mar 7 at 9:37
2

It sounds like what you actually need is two layers of templating, what I'm going to call the "headed notepaper" model.

a) Each service that wants to generate an email formats the body. This includes the card number, etc. Nobody outside the billing service needs to know the billing details.

b) The headed notepaper service (or function) formats that into a full HTML email, enforcing consistent branding across services

c) The email service is then simply handed a body, subject line, and metadata; it has the responsibility for handling SMTP. It also has some means of posting events back for nondeliverability, which will not be instant and may only happen after some time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.