I work on a big product at a big company and we have lots of backend services that have big responsibilities:
- The messaging service sends customers their emails, SMS messages, and push notifications.
- The banking service manages the customers’ bank accounts.
- The identity verification service verifies customers’ identities.
Except that they don’t.
Well, I suppose that from the perspective of its callers, the messaging service does indeed do the messaging:
fun completePaymentSuccessfully(
senderToken: CustomerToken,
senderName: String,
recipientToken: CustomerToken,
recipientName: String,
amount: Money,
note: String,
) {
// Notify the recipient.
messagingService.sendMessage(
recipientToken,
"Yo $recipientName! $senderName sent you $amount for $note."
)
// Notify the sender.
messagingService.sendMessage(
senderToken,
"Yo $senderName! $recipientName got your $amount for $note!"
)
...
}
But from inside the messaging service’s own codebase? It doesn’t implement messaging; it implements messaging glue:
- It decides which medium (email, SMS, push) to use for each message. This includes mapping abstract customer identifiers to concrete email addresses, SMS numbers, and APNS device tokens.
- It decides which route to use for each message. Perhaps we use two different email gateways (Amazon’s? Twilio’s?) for QoS, pricing and redundancy.
- It decides which language to write each message in. My sample code above is hardcoded English, but with fancy AIs is that really a problem?!
- It decides when to drop messages that exceed our configured limits. Limits apply per sending-service and per receiving-customer.
- It tracks a bunch of metrics for the benefits of the product (‘how many SMS messages did we send yesterday?’) and for its operation (‘why did delivery latency to gmail.com spike?’)
When I look inside the other services, I find that they don’t generally do the thing they promise to do. Instead they each build a nice abstraction over the thing, implement a bunch of business rules, and then delegate to a real thing to do the real job.
So What?
Despite the fact that these services ultimately outsource their One Job, they offer a ton of value to the people who use it. As a sender of messages, I don’t want to decide between Amazon and Twilio!
But it’s important to distinguish between the service’s high level pitch ‘it sends messages’ and it’s actual behavior, ‘it implements our product’s business rules for sending messages’.
If you fuck this up, you may find yourself in an design meeting where some pointy-haired architect suggests merging product A’s messaging service with product B’s messaging service: ‘They both send messages! It’s wasteful to maintain both!’.
Should you embark on such an effort, you might learn that product A’s messaging service makes different decisions than product B’s!
My War Story
Of course I made this mistake.
Product A had a service designed to broadcast a single marketing email to all of their customers. These messages weren’t urgent, but they needed wide distribution. The service’s business logic managed trickling out delivery over an hour or more.
Product B needed a service to send one-off transactional emails. These messages were urgent and should transmit immediately.
I did a ton of work to wedge product B’s use case into product A’s API, and it sucked. Then I did a ton of work to unwind this.
Business Logic & Glue
It’s difficult work to build a service that implements all of a product’s business rules.
It’s also difficult work to build a general-purpose service that’s agnostic of a company’s business rules.
When you build a service, you should decide what it is!