mohammedmanssour/laravel-recurring-models
Schedule recurring occurrences of any Eloquent model. Define a frequency and the package automatically creates the next instance, ideal for subscriptions, invoices, reminders, and other repeating records in Laravel. Supports flexible intervals.
Architecture fit. The package is trait-based and Eloquent-centric, which is the lowest-friction integration point in a Laravel codebase: use Recurring on a model, declare a recurrence, and let the package materialize future records. That fits conventional Laravel monoliths and modular monoliths well. It fits poorly with event-sourced or heavy CQRS designs, where generating cloned rows outside the domain's aggregate boundary creates a second source of truth. The core architectural decision to pressure-test is materialized occurrences vs. virtual expansion: if the package writes future rows to the table, then "one recurring series" becomes "N sibling rows," which changes query shape, list/filter semantics, and reporting. If it expands at query time, you inherit performance and pagination concerns instead. Verify which model you're buying before designing around it.
Integration feasibility. Expected effort is low-to-moderate: composer require, optional config/migration publish, add the trait, define the recurrence contract, and schedule the package's console command in routes/console.php/Kernel. No external services, no new infrastructure. Feasibility risk is therefore not technical difficulty but version-matrix compatibility and behavioral surprises in model lifecycle events — cloned records typically fire creating/created/saved, which means observers, notifications, and queued jobs wired to that model will fire for every generated occurrence.
Technical risk. Ranked:
(series_id, occurrence_date) and a lock.Key questions to put to the maintainer / PoC:
Stack fit. Best fit: Laravel 10–12 on PHP 8.1+, MySQL/Postgres, a working scheduler and queue, and an app that already treats the scheduler as a first-class production dependency. Poor fit if you run serverless with no reliable cron, if your recurrence needs are genuinely complex (RFC 5545-grade rules, exception dates, per-occurrence overrides), or if the target model has heavy observer chains.
Migration path. Prefer greenfield: apply the package to a new recurring capability rather than retrofitting an existing cron/queue generator. If replacing an existing generator, run both in parallel behind a feature flag, write to the same table with a source discriminator, and diff outputs for one or two cycles before cutting over. No data migration is required for new features; replacing an existing one is a data migration and should be scoped as such. Backfilling historical occurrences is usually unnecessary and should be explicitly declared out of scope.
Compatibility checks before commitment. Composer constraint resolution against your Laravel major; conflicts with other packages that hook model events (audit/logging packages, search indexers, activity logs) or that wrap save(); tenancy packages (stancl/tenancy) where the generator command must be tenant-aware; UUID/ULID primary keys where cloning must regenerate rather than copy the key; and any global scopes that would make generated rows invisible or wrongly tenant-scoped.
Sequencing.
RecurringScheduleService / repository) so the trait is used in exactly one place per model and the package is replaceable without touching domain code.composer.json, document the decision, and note the fork point.Product-side prerequisite. Someone must decide and document what "monthly" means on the 31st, whether occurrences are shifted or skipped, and what happens when the underlying template changes. This is a product decision the package cannot make for you.
Maintenance. MIT license, small maintainer base, recent release. Treat it as a dependency you may eventually own: budget roughly a few days per year for Laravel major-version upgrades and issue triage, and keep the adapter layer thin enough that a fork is a weekend, not a quarter. Watch the repo's release cadence as a leading indicator — an 18-month silence with no Laravel 13 support is the trigger to fork or replace.
Support. Community-only via GitHub issues: no SLA, no guaranteed response. Internal runbook ownership is mandatory. Escalation path is "read the source," which is realistic for a package of this size.
Scaling. Cost scales with rows, not with series. A daily series with 1,000 active parents and no end date is 365k rows/year before you notice. Mitigations: a bounded generation horizon (e.g., 60–90 days ahead) rather than generating to series end; a unique index on (series_id, occurrence_date) to make duplicates impossible rather than merely unlikely; archival or partitioning by date for long-lived tables; and moving generation to a queue if the per-run row count grows past what a single scheduled job can finish inside its window. Multi-tenant deployments multiply all of this by tenant count and may require per-tenant scheduling.
Failure modes and controls.
| Failure | Impact | Control |
|---|---|---|
| Scheduler down over a window | Missing occurrences, silent revenue/ops gaps | Alert on run-duration and run-count anomalies; verify backfill behavior; reconciliation query for expected vs. actual |
| Overlapping/retried runs | Duplicate occurrences, double sends | Unique DB constraint + atomic lock on the command |
| Generated rows fire observers | Duplicate emails, invoices, webhooks | Suppress events during generation or make consumers idempotent |
| DST / timezone drift | Occurrences shift by an hour, or land on the wrong day | Anchor to wall-clock; test the transition explicitly |
| Month-end series | Occurrences skip or bunch at month boundaries | Explicit, documented product rule; test the 31st→28th/29th case |
| Unbounded series | Unbounded growth and job cost | Require an end_date or a max-occurrence cap in the model contract |
| Template row deleted/edited | Orphaned or desynced occurrences | Define cascade behavior; decide edit semantics upfront |
Ramp-up. A mid-level Laravel developer can integrate the happy path in under a day; call it 2–3 days to production-ready once tests for DST, month-end, idempotency, and event suppression are included. The real ramp-up cost is not the API — it is the ~half-day of product and domain time required to define recurrence semantics precisely, plus an ops runbook covering what to do when a generation run is missed. Underinvesting in those two items is the most common way this integration fails quietly: not with an error, but with a table that is subtly missing rows nobody is counting.
How can I help you explore Laravel packages today?