Part 2 of 7 · Recurring invoice generator series ~7 min read

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.

How a contract becomes a self-rescheduling EventBridge entry, with a daily sweep as a safety net On the left, a contract box listing its cycle, anchor date, and term. An arrow leads to a “compute next due date” step in plain code, which branches by cycle type: monthly clamps to the last day of short months; quarterly steps three calendar months; per-milestone waits for a completion flag. The computed date feeds an EventBridge Scheduler entry, one per contract, that fires once on the due date. The fire triggers the Builder. After building, a final step computes the following due date and reschedules the same entry, shown as an arrow looping back from the builder to the scheduler entry. Separately, at the bottom, EventBridge Scheduler also runs a single daily catch-up sweep that scans the ledger for any invoice that should have been issued but wasn’t, and triggers the builder for it — the safety net if a fire is ever missed. A note reads: the schedule walks itself forward one cycle at a time; the daily sweep catches anything missed. Contract cycle, anchor date, term Compute next due plain code, branches by cycle type monthly — clamp short months quarterly — +3 calendar months per-milestone — on completion flag Scheduler entry one per contract, fires once on due date Builder builds the invoice fires reschedule for next cycle Daily catch-up sweep scans ledger for misses safety net The schedule walks itself forward one cycle at a time; the daily sweep catches anything missed.
Fig 2. A contract’s cycle, anchor, and term become one next-due date. EventBridge fires the builder on that date, which reschedules the entry for the following cycle. A daily sweep is the safety net for any missed fire.

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.
All posts