Quite often I'm faced with creating time-triggered emails, such as reminders. These email chains can get quite complicated, such as:
- User submits Form X
- If user does not do Action A after 24 hours, send Email 1
- Action A still not taken after 48 hours, send Email 2
- Action A still not taken after 1 week, send final Email 3
- Action A is taken, but Action B is not:
- 24h email
- 48h email
- 1w email
(if you're thinking, that's too many emails, I'm inclined to agree, this is just small snippet of 1 journey. Quite often there's several of these chains with many more points. I often push back to reduce them, it's just overly spammy in my opinion, but ultimately my job is to implement what's asked, not just refuse to do my job)
My current approach to this is often to use a regularly scheduled task (cron). Let's say every 10 minutes, fetch all FormX submissions without Action A that happened 24 hours ago (minus 10 minutes)
// Emails after 24h
whereBetween('created_at', [ now()->subDay()->subMinutes(10), now()->subDay() ])
// Emails after 48h
whereBetween('created_at', [ now()->subDays(2)->subMinutes(10), now()->subDays(2) ])
The above is a rough idea, but i'd also have to account for differences in seconds too.
I'm never truly happy with this approach, it seems fragile to either missing something, or sending duplicate emails because of a minor time gap or overlap. It's also intolerant to any failures. If the server goes down or into maintenance mode, then some emails won't be sent.
This is a common enough problem, there has to be a better way?
One idea I had was to queue up ALL of the emails when FormX is submitted, with a delay. Then when the job is run 24h later, it first checked if ActionA was taken, and if it was, just exits early and doesn't send. But that seems like a good way to fill the queue with lots of tasks that aren't needed.
Another idea was to create a 24h cache lock for every email/user pair, then I can afford to have my cron queries with several hours of overlap in the whereBetweens, duplicates will get filtered by checking the locks.
I'm sure most developers have had to tackle this, whats your strategy?