Skip to content

Part 3 of 7 · Consent preference keeper series ~5 min read

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

How a send decision is resolved from consent eventsA vertical chain of five steps entered by a box labelled May we send, carrying a person, a purpose and a channel. Step one asks whether it is in the cache, which is invalidated on write; a hit exits to Answer in under a millisecond. Step two finds the latest event for this purpose by timestamp; no event at all exits to the answer no. Step three asks whether it is granted; not granted exits to No, covering withdrawn or requested-only. Step four asks whether the channel is allowed, across email, SMS and post; if not it exits to No, because consent is per channel too. Step five is Yes, which is then cached. A note says absence of an event is a no, because silence has never been consent.AWS ACCOUNTMay we send?person, purpose, channelIn the cache?Cacheinvalidated on writeAnswersub-millisecondyesLatest event for this purposeby timestampNo event at allthe answer is nononeGranted?Nowithdrawn, or requested onlynoChannel allowed?email, sms, postNoconsent is per channel toonoYescache itAbsence of an event is a no. Silence has never been consent.
Fig 1. How the question is answered. Four of the five outcomes are no, which is the correct shape for a consent check.
  • 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

Why consent answers can be cached aggressivelyA horizontal row of five boxes. Asked constantly: on every send and every campaign. Changes rarely: a few events a day. Cache hard: for hours rather than seconds. Invalidate on write: rather than by expiry. Withdrawal is instant: which is the requirement. A note says invalidating on write rather than expiring is what makes a long cache safe here.READ CONSTANTLY, WRITTEN RARELYAsked constantlyevery send, every campaignChanges rarelya few events a dayCache hardhours, not secondsInvalidate on writenot by expiryWithdrawal is instantwhich is the requirementInvalidating on write rather than expiring is what makes a long cache safe here.
Fig 2. The access pattern that makes aggressive caching both possible and safe. The requirement is that a withdrawal is immediate, and invalidation gives you that without a short TTL.
  • 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