
“A policy applied inconsistently is worse than one not applied at all.”
The cron jobs were the business
Six daily jobs that handled renewal, downgrade, dunning and re-engagement — the revenue cycle, running on a schedule.
Scheduled jobs are usually maintenance: clean this up, expire that, send a digest. On this platform they were where the money was.
Six of them ran daily. Between them they reminded advertisers before expiry, downgraded listings that fell too far behind on payment, processed recurring charges, chased unverified listings, ran a re-engagement campaign timed to the annual cycle, and warned fleet operators about upcoming vehicle inspections.
The problem
The constraint
What we did
We made renewal the central job
It finds listings approaching expiry, sends reminders, and — for listings more than seven days late — automatically downgrades them, updates the invoice, and re-indexes the record so the change is visible in search immediately.
Seven days, then downgradeA downgrade you cannot see in search is not a downgrade
It is a downgrade in the database only, and the advertiser keeps the placement they stopped paying for. That is why re-indexing belongs in the same job rather than in a nightly batch.
Visible immediatelyWe handled recurring payments with all three outcomes
Success, failure and upcoming charge each have their own message. Payment failure is not an error state to log — it is a conversation with a customer who probably wants to keep the service.
A conversation, not a logWe timed re-engagement to the annual cycle
A three-step campaign at roughly 179, 182 and 186 days from a listing’s last update. Not arbitrary: half a year out from renewal, a listing with stale information is a listing whose owner has stopped thinking about it — and that is who fails to renew.
179 · 182 · 186 daysWe chose the channel per job, and kept one route out
Reminders about unfinished draft listings go in-app rather than by email: someone halfway through creating a listing doesn’t need to be chased into their inbox, they need to be met where they left off. Every job followed the same shape — query the database, take a template from object storage, write a record, put a message on a queue — and one service did the actual sending. Outside production that service redirected all mail to a test address, which made emailing real customers from staging structurally impossible.
One route out, one redirect
Every job starts with a query: listings about to expire, charges coming up, listings untouched for 179 days. The criterion is commercial policy written as a condition, not as someone’s judgement.
The result
- Six daily jobs carrying renewal, downgrade, dunning and re-engagement
- Commercial policy applied identically to every account, without anyone deciding to apply it
- 55 email templates behind a single sending service
What we’d do differently
The jobs ran daily and reported nothing but errors. Nobody could see how many listings had been downgraded this week, or whether the re-engagement campaign moved anything at all. Business logic that runs unattended needs a business-level report, not just a log — otherwise you find out it stopped working when someone asks why revenue dipped.
- AWS Lambda
- SQS
- MySQL
- Handlebars
- Node.js