- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Building Contract Renewal Reminders That Don't Annoy Anyone
About Post
On paper, a contract renewal reminder is the easiest feature in the backlog. Find contracts ending soon, send an email. A scheduled job and a notification. Half a day, tops.
Then reality joins the meeting. The same tenant gets the reminder twice because the job ran twice. A contract added to the system late gets three reminders on the same morning. Someone who renewed last week is still being asked to renew. And the property manager receives forty separate emails before lunch.
Kate PMS handles the full life of tenancy contracts, from e-signing to rent to renewals, across more than 2,000 contracts. At that scale, "send a reminder" is really a small system. I won't go into how our implementation works internally, but here are the design principles I think every reminder system needs, with a simplified Laravel sketch of the patterns.
Principle 1: a reminder is a schedule, not a moment
The naive query looks for contracts ending exactly 30 days from today. It works perfectly until the scheduler skips a day (a deploy, a server restart, a failed job). Every contract that hit its 30-day mark on that day silently gets no reminder at all.
A sturdier approach treats each reminder as a stage that becomes due and stays due until it's handled. "The 30-day reminder is due if the contract ends within 30 days, hasn't ended yet, and we haven't recorded sending it." If a run is missed, the next run catches up.
$today = CarbonImmutable::today('Asia/Qatar');
foreach (self::STAGES as $stage => $daysBefore) { // e.g. ['first' => 90, ...]
Contract::query()
->where('status', 'active')
->whereNull('renewal_decided_at')
->whereDate('ends_on', '>=', $today)
->whereDate('ends_on', '<=', $today->addDays($daysBefore))
->whereDoesntHave('renewalReminders', fn ($q) => $q->where('stage', $stage))
->chunkById(200, fn ($contracts) => $contracts->each(
fn (Contract $contract) => SendRenewalReminder::dispatch($contract, $stage)
));
}
Notice the immutable date and the explicit time zone. "Thirty days before the end" should mean the business's calendar, not the server's UTC clock.
Principle 2: lead times should match the decision
How early should you remind? Work backwards from the decision people need to make. Renewing a lease isn't a one-click choice: the tenant may need to discuss terms, the owner may want to adjust rent, and there may be a notice period in the contract.
So the stages should come from the business, not from what feels tidy. A sensible pattern is an early heads-up, a reminder when a decision is actually needed, and a final one close to the end date. Keep the numbers in config, not scattered through code, because they will change.
And handle the catch-up edge: if a contract enters the system with 40 days left, it should get the most relevant reminder, not every earlier stage at once. Record the skipped stages as skipped so they don't fire later either.
Principle 3: know when to stop
Nothing annoys people faster than a reminder about something they've already done. Every reminder query needs clear stop conditions:
- The contract has been renewed, or a renewal is in progress.
- Notice of non-renewal has been given.
- The contract was terminated early or replaced.
- A staff member has marked it as being handled personally.
A single field like renewal_decided_at, set by any of those actions, keeps the query simple. And check it again right before sending, inside the job, because the situation may have changed between the query and the send.
Principle 4: every send is idempotent
Jobs get retried. Schedulers occasionally run twice. Two servers may both think they're in charge. The reminder must still go out once.
The reliable tool is the same one that makes payment retries safe: a database row with a unique constraint, claimed before sending.
public function handle(): void
{
if ($this->contract->fresh()->renewal_decided_at) {
return; // decided since this job was queued
}
// unique index on (contract_id, stage)
$claimed = DB::table('renewal_reminders')->insertOrIgnore([
'contract_id' => $this->contract->id,
'stage' => $this->stage,
'created_at' => now(),
]);
if ($claimed === 0) {
return; // another run already sent it
}
$this->contract->tenant->notify(new RenewalReminder($this->contract, $this->stage));
}
There's a trade-off hiding here. Claiming first means that if the email provider fails right after the claim, the reminder is recorded but never delivered (at most once). Sending first risks duplicates (at least once). For reminders, I prefer claim-first plus a status column: mark the row as sent or failed, and let failed rows be retried. A missed reminder can be caught; a duplicate can't be unsent.
On the scheduling side, Laravel helps too: withoutOverlapping() stops a slow run from overlapping the next, and onOneServer() stops several servers running the same task (it needs a shared cache that supports locks, like Redis or the database driver).
Principle 5: respect how people want to hear from you
Tenants and staff are different audiences with different needs.
- Tenants want a clear message on the channel they actually read: a push notification in the tenant app, an email, maybe an SMS for the final reminder. Let them choose where reasonable, but treat contract notices as operational messages, not marketing. They shouldn't silently vanish because someone unsubscribed from newsletters.
- Staff don't want forty emails. They want a digest: one message a day listing the contracts that need attention, sorted by urgency, with links.
- Timing matters. Send at a sensible local hour, not at whatever minute the job happens to run.
Laravel's notifications make channel choice natural: the via() method can read the recipient's preferences and return the right channels for each person.
Principle 6: escalate, don't repeat
If a tenant hasn't responded after the second reminder, sending the same message a third time to the same person rarely helps. It's better to escalate: put the contract on the responsible staff member's list so a human follows up. Reminders start the conversation; people finish it.
The checklist: stages that stay due until handled, lead times from the business, clear stop conditions checked twice, a unique claim per stage, digests for staff, preferred channels for tenants, and escalation to a human instead of endless repeats.
The real measure
A good reminder system is one nobody talks about. Tenants get one clear message at the right time, staff see a tidy list instead of a flood, and no renewal date sneaks up on anyone. It's not glamorous work, but it's the kind of detail that makes software feel trustworthy.
How do you handle reminders in your apps: fixed dates, due stages, or something smarter? And how do you stop them from nagging?

Be first to comment it...