How a billing schedule gets set up
Before a single invoice can go out on time, the system has to know exactly when each one is due — and “monthly” hides a surprising amount of detail. This post is about that step alone: how a contract’s cycle, anchor date, and term become a concrete schedule, and how each contract gets its own EventBridge Scheduler entry that fires on the right day and nothing more.
Key takeaways
- A contract’s cycle, anchor date, and term are turned into a concrete next-due date — “monthly” on its own isn’t enough.
- Each contract gets its own EventBridge Scheduler entry, set to fire once on its next due date, then rescheduled for the one after.
- Three cycle types are supported: monthly, quarterly, and per-milestone — the first two by date, the third by an event.
- A signup part-way through a cycle is prorated on the first invoice; the schedule itself stays anchored to the clean date.
- A daily catch-up sweep is the safety net — if a fire is ever missed, the next sweep builds the invoice that should have gone out.
From a contract to a due date
“Bill them monthly” sounds like a complete instruction until you try to turn it into a date. Monthly from when — the day they signed, or the 1st? Does a contract signed on the 31st bill on the 31st of a month that only has 30 days? Does a quarterly contract anchored to 15 January fire in April, or three calendar months later to the day? The schedule is the part of the system that has to answer all of this concretely, because a scheduler doesn’t take “monthly” — it takes a timestamp.
Each contract carries three fields that pin its timing down. The cycle says how often: monthly, quarterly, or per-milestone. The anchor date says what the cycle counts from — usually the day the contract starts, but it can be normalised to a clean day like the 1st so a whole book of contracts bills together. The term says when to stop — an end date, a fixed number of cycles, or open-ended until cancelled. From those three, a small piece of plain code computes one thing: the next date this contract is due. That date, and only that date, is what goes to the scheduler.
The three cycle types
- Monthly. The common retainer. Anchored to a day of the month; if that day doesn’t exist in a shorter month (the 31st in February), it clamps to the last day. A retainer anchored to the 1st bills on the 1st of every month; one anchored to the signup day of the 18th bills on the 18th.
- Quarterly. Every three months from the anchor, by calendar month rather than by 90 days, so a contract anchored to 15 January bills on 15 January, 15 April, 15 July, and 15 October — predictable dates a customer can plan around.
- Per-milestone. No calendar at all. The contract lists milestones (a phase, a deliverable, a stage payment), and an invoice is due when a milestone is marked complete — a flag flipped in the contract sheet. This is the project-work case, where billing follows delivery rather than the calendar.
One schedule per contract
The obvious design is a single nightly job that scans every contract and asks “is anything due today?” It works, but it scans the whole book every night to act on a handful of rows, and a bug in the scan can miss or double-bill across all of them at once. This system does the opposite: each contract gets its own EventBridge Scheduler entry, set to fire once, at the exact next due date. When that fire builds the invoice, the last step is to compute the following due date and reschedule the same entry for it. The schedule walks itself forward, one cycle at a time, and a contract that isn’t due simply has no activity that day.
Per-milestone contracts don’t get a date-based entry; they get a small trigger that fires when the milestone flag flips during a sync. Either way, the unit of scheduling is the contract, so pausing, cancelling, or changing one contract’s timing never touches another’s.
Signing up mid-cycle
A contract rarely starts on a tidy date. A client signs a £1,200/month retainer on 18 June and wants to be billed on the 1st like everyone else. There are two clean ways to handle the gap, and the contract’s setup chooses one. Either the anchor is the signup day (the 18th) and they simply bill on the 18th forever — no proration needed. Or the anchor is normalised to the 1st and the first invoice is prorated: June has 30 days, the client was active from the 18th to the 30th, that’s 13 days, so the first bill is £1,200 × 13/30 = £520, and every invoice after that is the clean £1,200 on the 1st. Either way the schedule stays simple — it always points at a clean recurring date. Proration is a builder concern, not a scheduler one, and it’s exactly what Part 4 works through with numbers.
Why a daily sweep as well
Per-contract schedules are precise, but “fire exactly once on the right day” is a promise worth backing up. Things go wrong: a deploy lands mid-fire, a downstream queue is briefly unavailable, a schedule gets left paused after maintenance. So one more EventBridge entry — a single daily catch-up sweep — scans the ledger each morning and asks a blunt question: is there any contract whose due date has passed without an invoice in the ledger? If so, it builds the one that’s missing. Because every invoice carries the contract id and the cycle it belongs to, a re-run can never produce a duplicate — the builder checks the ledger before it writes, so the sweep can only ever fill a genuine gap. It’s the cheapest possible insurance: one extra scheduled function whose whole job is to make sure nothing silently fails to bill.
Why this shape
- The scheduler only ever holds dates. “Monthly” is resolved to a concrete next-due date in plain code before anything is scheduled.
- One entry per contract. Pausing or changing one contract’s timing never disturbs another’s.
- The schedule self-advances. Each fire reschedules itself for the following cycle, so the book stays current with no central scan.
- Milestones are events, not dates. Per-milestone billing fires on a completion flag, keeping project work out of the calendar logic.
- A daily sweep is the safety net. If a fire is ever missed, the sweep builds the missing invoice — and the ledger check makes a duplicate impossible.