How a send decision is resolved
The resolver has one job and its interface is deliberately tiny: given a person, a purpose and a channel, may we send. Everything about the design is in service of that being fast, consistent and safe when it breaks.
Key takeaways
- One question: person, purpose, channel, and a yes or no.
- The answer is computed from the latest event per purpose, with no ambiguity.
- Cached aggressively, and invalidated the instant an event is written.
- Unreachable means no. A resolver that fails open is worse than no resolver.
- Bulk resolution exists for campaigns, and returns the same answers.
One question
- Database
- App integration
- Machine learning
- Security & identity
- Management
- Analytics
Absence is a no
A person with no consent event for a purpose has not consented to it, and the resolver says no. That sounds obvious and is worth stating because the alternative implementation — defaulting to yes for anybody not explicitly suppressed — is exactly how most legacy mailing lists work and is the thing this system replaces.
Per channel as well as per purpose
Somebody can reasonably agree to marketing by email and not by SMS, and a great many people feel differently about the two. Making channel part of the question rather than part of the purpose keeps the purpose list short while still supporting that, and it means adding a channel later does not require re-consenting anybody.
Caching
- Database
- Machine learning
- Management
- Analytics
A short TTL is the instinctive answer and it gets the requirement wrong in both directions: five minutes is both far too much latency on a withdrawal and far too much load for data that changes twice a day. Invalidating the specific person’s entry when an event is written gives a genuinely instant withdrawal and lets everything else be cached for hours.
Failing closed
If the resolver cannot be reached, the sending system must not send. That is uncomfortable — it means a resolver outage stops a campaign — and it is not close to a difficult decision.
Why fail closed is the only option
- Failing open sends to people who withdrew. That is the exact harm the system exists to prevent, and it happens at campaign scale rather than one message at a time.
- Failing closed delays a campaign. Nobody has ever been harmed by a newsletter arriving on Thursday instead of Wednesday.
- It has to be the default in the client. A sending system that treats a timeout as permission has failed open regardless of what the resolver intended.
- And it has to be tested. Point a sending system at a dead resolver and confirm it sends nothing. It is a five-minute test that most implementations have never run.
Bulk resolution
A campaign to twelve thousand people should not make twelve thousand calls. A bulk endpoint that takes a list and returns the allowed subset is the same logic with the same cache, and it is worth building early because the alternative is somebody exporting a list once and using it for three campaigns.
The bulk response is deliberately the allowed subset rather than a per-person verdict list, which makes the wrong usage awkward: you cannot accidentally send to somebody who was not returned.
Next: making a withdrawal real everywhere.
All posts