Recurring Events in Rails
Modeling recurring events in a calendar is an interesting problem to solve. There are many different scenarios in which you might need to model recurring events, but in this article I will walk through a simple example of weekly recurring events in a Rails app.
Although this post covers most of the code needed for the example, I will skip over some of the details (such as views), and you will have to fill in the blanks in such places.
Much of the code here is taken from a real project I’m working on, and I’ve tried to simplify the code as much as possible. However, some of the tradeoffs that I needed in my project aren’t really needed in this example. I have pointed out some cases where this is true.
Scheduling weekly events
Let’s take an example where we will need to model weekly recurring events:
Our users want to be able to save weekly events, with a day of the week and a time. The system will perform an action every week on that day and time. For example, a user could set up a reminder (“remind me to send status report at 5pm every Friday”), and the system should send an email at that time every week.
Modeling recurring events
Before we start writing the code for scheduling events, let’s briefly think about how we will implement this.
We will start with a
which will contain the day and time at which the event must be performed.
We could also call this model something like
but since we could have other things in future that could be recurring,
I prefer having a
RecurringEvent model and associate that with other models.
To keep things simple in this post,
we will just have a
reminder string field in the
The user will create a recurring event using a form that contains the following fields:
- reminder (eg. “Send weekly status report”)
- time - we will just save a string containing the time, (eg. “5:00 pm”)
- day - day of the week, as an integer (0 for Sunday, 1 for Monday, etc)
We will use an
Event model to save the actual time at which
We will save the next instance of the recurring event in the
We will also use
ActiveJob to schedule the event at the correct time
ActiveJob class will execute the action that should happen at the time
(sending the reminder email, in this example),
and also create the next instance of the event.
Creating a new RecurringEvent
Let’s start off by creating the
We will start with a new page for creating a new recurring event by implementing
The form (
for adding a new recurring event looks like this:
The next thing to do is to implement the
This is where things get a little interesting.
I like to put this kind of business logic code in
RecurringEvents::Create must do three things:
- save the recurring event
- calculate the date of next instance of the event and save it
- schedule a background job to run at that time
We will come back to the
but first we need an
where we store the next occurence of a recurring event.
We could simplify this a great deal
run_at a field in
but I’ve added a separate model in my project because
I’d like to log other information of the occurence later on.
Calculate next date for the event
We also need to calculate the date
for the next occurence of a recurring event.
For this, I use another plain Ruby class in
that takes a
RecurringEvent instance and returns a date.
One thing I haven’t addressed in this code is timezones, and we will fix that later on in this article.
Now that we have everything in place, let’s look at
Here we just persist a
RecurringEvent to the database,
Events::Schedule service class to create a new instance of
run_at field set correctly.
Let’s look at the
Events::Schedule class, which does the following:
- Creates an instance of the
Eventclass and sets the
- Schedules a background job that will run at
Event#run_at. This will be handled by the
RunEventJob should contain the code for whatever should happen at the time,
and at the end calls
to queue up the next occurence of the event.
Remember the timezone problem I mentioned in
When a user wants to send a reminder at “Friday 5PM”,
they usually mean 5pm on Friday in their timezone.
We should make sure that the event time that we calculate
uses the correct timezone offset.
In this article I have not mentioned a
Let’s assume that we do have such a model,
and we have associated
RecurringEvent as belonging to a user.
We will also need to be aware of the user timezone.
This is another thing that I will not be addressing in this article,
but let’s assume that we can access the user’s timezone as
(My previous article on
setting user timezones during user signup
might be useful at this point.)
With that, we now have a working weekly recurring events system. There are many more things to consider, though, and I will list some gotchas below.
A couple of gotchas you might need to consider if you’re building a similar feature:
- When you allow users to delete a
RecurringEvent, you might see the
RunEventJob. This exception could also be caused by other factors, such as the database being down. We need to always retry unless the exception is
ActiveRecord::RecordNotFound. The following code achieves that:
- When users edit a
RecurringEvent, make sure that you update the next
As I mentioned at the start of the article,
there are places where I’ve retained code
that isn’t needed in a simple example like this one.
For instance, I needed a model for each event occurence
because I need to add additional information there,
but here you could just use a field in the
There are other scenarios in which you might need recurring events, such as “second Saturday of every month”, and this article doesn’t cover such advanced scenarios.
If you’re looking a more complex example, take a look at Martin Fowler’s excellent article [PDF] on modeling recurring events using temporal expressions.
Update: As Swanand points out in the comments, the recurrence rules section in iCalendar specification (RFC 5545) is an interesting read if you’re interested in exploring further.