Reducing Code Motion: Streamlining Content Scheduling in Aggregate
Learn how a refactor of the Aggregate content feed eliminated a complex 'SCHEDULED' state and cron job, simplifying content scheduling using a `publicationDate` field.
Written on 2025-10-22
I recently undertook an interesting refactor on Aggregate, my community-driven content feed. Aggregate crawls content from various sources across the web and bundles it into a single RSS feed. To avoid overwhelming subscribers with too much content at once, it features a "schedule feature" that limits new posts to three per day, queuing any excess for subsequent days.
Initially, a post's lifecycle involved three states: PENDING, SCHEDULED, or PUBLISHED. The transition from PENDING to another state was a manual user action—which is central to Aggregate's premise of hand-picked content. Moving from SCHEDULED to PUBLISHED was previously handled by a cron job; however, my recent refactor changed this entirely.
From a human perspective, this "scheduling" process made sense: a post is scheduled, then published at the appropriate time. This automatic, time-driven transition introduced considerable complexity into the codebase:
- A cron job that ran periodically to pick
SCHEDULEDposts and transition them toPUBLISHED. - An interface between OS-level cron jobs and the application code, typically a console command.
- Logic to differentiate whether a post should be
SCHEDULEDorPUBLISHEDimmediately if there was capacity. - Extensive tests to verify: 1) the command correctly performed state transitions, and 2) it correctly interacted with the OS-level cron to run daily.
This entire process involved what I like to call 'motion' – numerous interdependent moving parts. The cron job, the console command, and the logic to decide if the scheduled state could be skipped all contributed to increased complexity in testing, maintaining, and debugging.
When I recently rebuilt Aggregate with Tempest, I seized the opportunity to simplify this flow. I realized that a single change could achieve this: I removed the SCHEDULED state entirely and incorporated a publicationDate field into the PUBLISHED state. The key insight was allowing a publicationDate to be set in the future.
Now, whenever a post is published, I query the database to find the first "free slot" using this logic:
SELECT
publicationDate
FROM
posts
WHERE
publicationDate > :publicationDate
AND state = "PUBLISHED"
GROUP BY
publicationDate
HAVING
COUNT(*) >= 3
ORDER BY
publicationDate DESC
This query identifies the farthest day in the future that already has three or more published posts. We then simply add one more day to it to determine the next available slot:
$nextAvailableDate = $futureDate->plusDay()->startOfDay();
And just like that, we've eliminated a significant amount of 'motion':
- No more cron job.
- No more automatic state transitions.
- No more need to differentiate between instant publication and scheduled publication.
Of course, this solution isn't without its downsides. Determining whether a post is "visible" now requires checking both its state and its publicationDate. There's also the question of how to handle cases where a future scheduled post is removed, potentially opening up a slot "in between" existing posts (though this could be addressed by adjusting the query).
Nonetheless, these are trade-offs to consider. We've exchanged one form of complexity for another. In my specific case, the payoff in simplicity and reduced maintenance is well worth it. As with all software development, every approach has its pros and cons—"it depends."
For me, the most important lesson from this refactor is that modeling a process strictly from a "human point of view" doesn't always yield the simplest or most optimal technical solution. It's often beneficial to spend time translating between the human description and the technical problem-solving approach, as they may not map directly.
Perhaps you have some thoughts? For the very first time in the history of this blog, you can leave them here, right on this page (simply scroll down). If anything were to go wrong (because it's the first time 😅), you can still send me an email.