email-alert-api: Decision Record: MVP implementation of Digests
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.
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.
There are two classes to represent the respective digest periods:
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.
- Be interfaced by a scheduling service to run at a set time appropriate for their period
- Create a
- 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
- Be configurable as to what times a digest runs from and until
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
- 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
- Store a timestamp for when all the emails were created for the digest, to act as indicator that it is complete.
- 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.
Responsible for taking a
DigestRun instance and using that to
determine which subscribers should receive an email for the digest period.
- 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
- Return a list of ids referencing
- Return an ActiveRecord scope to allow the caller to deal with this returning an unlimited output
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.
- An association to
- An association to
- An association to
- Store a timestamp for when it has been completed, can be used therefore to
work out if a
DigestRunis complete or not.
A worker entity that accepts the argument of a
DigestRunSubscriber and has the responsibility to
- Pass the
SubscriptionContentChangeQueryto retrieve a collection of
SubscriptionContentChangeQuery::Resultinstances. Each of these represents a
ContentChangesassociated with that subscription.
- Interface with
DigestEmailBuilderto build an
SubscriptionContentinstances for each
- Mark a
DigestRunas completed if all
DigestRunSubscriberare complete for the
For a given
DigestRunSubscriber this will determine
ContentChanges associated with each
Subscription for the digest
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
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
SubscriptionMatcher to have this suffix too.
- Return an array containing
SubscriptionContentChangeQuery::Resultinstances will contain details of the
ContentChangeinstances for that
Subscription. This is to allow rendering the Email with
ContentChangesin context with their
- Only return necessary fields to limit memory usage/execution time
DigestEmailBuilder will take arguments of
DigestRunSubscriber and a collection of
SubscriptionContentChangeQuery::Result objects. It will take this data and
use this to create an
Formally this type of class was suffixed with Renderer eg
however we felt that the class responsibilities should include persisting
- Create an
- Create an unsubscribe link
- Be distinct from the email builder used to create immediate emails
- Be responsible for deciding what to do with duplicate
In order to implement these changes a number of existing areas were identified:
EmailRenderer should be changed
We intend to rename the
EmailRenderer class to reflect that it is for
ContentChanges. This will be renamed
We also intend to invert the responsibility of
EmailRenderer and instead of the instance of
an instance of
SubscripionContent is a model that was intended to associate
This however becomes problematic when we introduce
instances which are for digests and should not be processed as an immediate
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