For a long time, I believed recurring events were a solved problem. After all, I’ve successfully used crontab
countless times for backups and cleanup scripts. However, when you need to model recurring events within your application, especially for user facing features, the complexity becomes apparent. This post discusses the technical challenges of implementing recurrence patterns and my preferred way to deal with them.
Choosing a Recurrence Format
The first rule of implementing recurrence: don’t invent your own format. While it might seem straightforward for simple patterns, custom formats inevitably lead to painful migrations and refactors as requirements evolve. As I see it, you have two primary options, each suited to different use cases.
CRON Expressions
As developers, our first instinct might be to reach for CRON syntax:
* * * * * [*]
│ │ │ │ │ └─ (optional) Year
│ │ │ │ └──── Day of week (0-7, Sunday = 0 or 7)
│ │ │ └────── Month (1-12)
│ │ └──────── Day of month (1-31)
│ └────────── Hour (0-23)
└──────────── Minute (0-59)
CRON excels at time based system scheduling with ranges and steps. It’s perfect for tasks like “every 5 minutes” or “daily at 2 AM.” However, CRON has significant limitations for business applications:
- Cannot express patterns like “the 2nd Tuesday of every month”
- No support for bounded recurrence (“repeat 10 times then stop”)
- No date based termination (“repeat until December 31st”)
- Lacks timezone awareness in standard implementations
RRULE (iCalendar Specification)
RRULE (Recurrence Rule) from the iCalendar specification offers far more flexibility through key value pairs. Here’s how you express “every weekday”:
FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;INTERVAL=1
RRULE supports complex patterns including:
- Positional weekdays (“2nd Tuesday”)
- Bounded occurrences (
COUNT=10
) - Date based termination (
UNTIL=20251231T235959Z
) - Exception dates for holidays
- Timezone aware recurrence
The tradeoff is complexity. The full RRULE specification is extensive, so consider limiting your implementation to a subset that matches your domain requirements. This also simplifies any UI components needed for pattern configuration.
Designing the data model
The most flexible approach separates event instances from their recurrence patterns. This design enables efficient querying of upcoming events while preserving the ability to modify future occurrences without affecting historical data. Since event instances are largely domain dependent, we’ll focus on modeling the recurrence patterns.
Recurrence data can be stored as a standalone model or integrated into an existing one. At minimum, store these fields alongside your recurrence rule:
Field | Type |
---|---|
start_timestamp_utc | Timestamp with timezone |
end_timestamp_utc | Timestamp with timezone |
recurrence_rule | String |
Clear start and end timestamps make pattern retrieval significantly more efficient, particularly in relational databases where these fields can be indexed. Store timestamps in UTC to prevent timezone and DST issues.
Also consider these additional fields based on your requirements:
duration
: For events with specific lengthscreation_lead_time
: Lead time before an event when it should be created. This makes it configurable when the event should already be visible to the user. In some application domains this is a requirement and it can also be handy to see upcoming events. From a technical perspective this can also be used to limit the amount of events that are created as we will discuss later.metadata
: JSON field for domain specific attributesis_active
: Boolean flag to enable or disable patterns without deletion
Generating the Events
I would strongly disadvise against generating all possible events upfront. Unbounded patterns make this impossible in the first place. Instead make use of lazy generation within controlled windows. The creation_lead_time
provides control over the generation boundaries. A 30 day lead time ensures users see events a month in advance without generating years of data. This naturally bounds even infinite patterns while meeting user expectations for visibility.
In general you’d need to implement the following steps:
- Periodically fetch all your active recurrence patterns:
- The interval in which you do this depends on the granularity of your recurrence patterns.
- Start and end timestamps make querying more efficient, especially if you have the right indices.
- If using lead times, you must take this account in the fetching logic. For instance, to generate events for “today”, you need to fetch patterns where the event date minus the lead time is less than or equal to today.
- For each recurrence pattern, generate the necessary event instances:
- Generate multiple instances in a single batch to reduce database roundtrips.
- If you expect a lot of events, paginate instance generation to avoid memory issues.
Whenever a new recurrence rule is added (or you need to query the events on demand), you can trigger this logic to immediately generate/get back the events. I would also recommend to monitor the growth of your event instances table and implement archival strategies (using cronjobs
for instance) for past events if necessary.
Again, there are many approaches to handle recurrence, and hopefully this post has given you some insights and highlighted the key decisions you’ll face.