email-alert-api: Decision Record: MVP implementation of Digests
Introduction
In January 2018 we started one of the latest major pieces of functionality that would be required to meet our MVP (minimal viable product) and allow us to launch a Notify powered iteration of Email Alert API.
This document serves to outline the architecture we have planned for this and explain the purposes of the individual components.
Specifics
Product decisions
The need for a change in architecture was determined by product decisions which differed from the previous understanding of digest function explored in adr-001.
The biggest decision that influenced the change was an intention to send each user a single digest email that contained all of their topics as is consistent with Govdelivery. We had previously intended to launch with the change that each subscription is a separate email.
A change from Govdelivery is that we will store the delivery preference (immediate, daily, weekly) at a subscription level rather than a user level. This will mean that a user can be signed up to receive emails to some lists immediately and daily/weekly digests for others.
Diagram

Components
DigestInitiatorService
There are two classes to represent the respective digest periods:
DailyDigestInitiatorService and WeeklyDigestInitiatorService.
Their responsibility is to start the process to create digests and to ensure that multiple digests are not started concurrently.
Initially this class was named DigestSchedulerService however we felt that
this was ill fitting with the classes actual responsibilities.
Shall
- Be interfaced by a scheduling service to run at a set time appropriate for their period
- Create a
DigestRunmodel - Ensure that multiple digests for the same period are not run concurrently
- Interface with
DigestRunSubscriberQueryto determine which subscribers are due to receive emails for this digest
Should
- Be configurable as to what times a digest runs from and until
DigestRun
A model which represents a distinct run of a period of digest (daily or weekly).
Has a responsibility to persist data that is related to a full digest run for all subscribers.
We initially named this Digest however it transpired that there was already
a module in Ruby standard library named Digest.
Shall
- Store information such as:
- Date digest is run on
- The period the digest is for
- The start time for the digest period
- The end time for the digest period
Should
- Store a timestamp for when all the emails were created for the digest, to act as indicator that it is complete.
May
- Have a unique index for digest date and period, however this would limit ability to re-run a digest in the case of a problem.
DigestRunSubscriberQuery
Responsible for taking a DigestRun instance and using that to
determine which subscribers should receive an email for the digest period.
Shall
- Use a
DigestRunto determine which subscribers should receive an email for a digest period - Determine only the subscribers who will be due to be notified of at least one content change
Should
- Return a list of ids referencing
Subscribers
May
- Return an ActiveRecord scope to allow the caller to deal with this returning an unlimited output
DigestRunSubscriber
A model which is used to associate a DigestRun with a
Subscriber instance. Will mostly be used as a persisted piece of data that
logs which subscribers receive an email due to a digest. It is unlikely to be
needed long term.
Shall
- Store
- An association to
DigestRun - An association to
Subscriber
- An association to
Should
- Store a timestamp for when it has been completed, can be used therefore to
work out if a
DigestRunis complete or not.
DigestGenerationWorker
A worker entity that accepts the argument of a
DigestRunSubscriber and has the responsibility to
create an Email entity for the associated Subscriber.
Shall
- Pass the
DigestRunSubscribertoSubscriptionContentChangeQueryto retrieve a collection ofSubscriptionContentChangeQuery::Resultinstances. Each of these represents aSubscriptionandContentChangesassociated with that subscription. - Interface with
DigestEmailBuilderto build anEmailentity - Create
SubscriptionContentinstances for eachContentChangeperSubscripption
Should
- Mark a
DigestRunSubscriberas completed - Mark
DigestRunas completed if allDigestRunSubscriberare complete for theDigestRun
SubscriptionContentChangeQuery
For a given DigestRunSubscriber this will determine
all the ContentChanges associated with each Subscription for the digest
period.
This class began as an analogue of SubscriptionMatcher - which finds
Subscriptions associated with a ContentChange - we decided however that
this would require an additional class to interface with DigestRunSubscriber
and so decided to have this no longer be an inverse. We also decided that the
name should be suffixed with Query rather than Matcher and that we should
rename SubscriptionMatcher to have this suffix too.
Shall
- Return an array containing
SubscriptionContentChangeQuery::Resultinstances - The
SubscriptionContentChangeQuery::Resultinstances will contain details of theSubscriptionand theContentChangeinstances for thatSubscription. This is to allow rendering the Email withContentChangesin context with theirSubscription.
May
- Only return necessary fields to limit memory usage/execution time
DigestEmailBuilder
A DigestEmailBuilder will take arguments of
DigestRunSubscriber and a collection of
SubscriptionContentChangeQuery::Result objects. It will take this data and
use this to create an Email instance.
Formally this type of class was suffixed with Renderer eg EmailRenderer
however we felt that the class responsibilities should include persisting
an Email instance and therefore was a creation pattern.
Shall
- Create an
Emailinstance - Create an unsubscribe link
Should
- Be distinct from the email builder used to create immediate emails
- Be responsible for deciding what to do with duplicate
ContentChangeentries
Supplementary changes
In order to implement these changes a number of existing areas were identified:
Email and EmailRenderer should be changed
We intend to rename the EmailRenderer class to reflect that it is for
immediate ContentChanges. This will be renamed ImmediateEmailBuilder.
We also intend to invert the responsibility of Email calling an instance of
EmailRenderer and instead of the instance of ImmediateEmailBuilder create
an instance of Email
Iterating SubscriptionContent
SubscripionContent is a model that was intended to associate ContentChange
entities with Subscription and Email. An absence of an email model was an
indication this required processing for an immediate email.
This however becomes problematic when we introduce SubscriptionContent
instances which are for digests and should not be processed as an immediate
email.
We decided the way to resolve this was to introduce an additional field to
SubscriptionContent which is a foreign key to DigestRunSubscriber. A null
entry for this column would indicate an email is intended for immediate
processing.