The task was scoped as a small automation: North America only, one kind of event, some registrations and a confirmation email. The kind of thing a spreadsheet and a script would cover.
It was not that, and the tell came early. The events were multi-session, the attendees were educators across several timezones, and completion had to produce a certification that somebody would later rely on. Each of those is survivable alone. Together they mean state: who registered for which session, who actually attended, what that leaves outstanding, and what has to happen next and when. A spreadsheet holds state right up until two things change at once.
Where the complexity actually lives
Almost none of it is in the event model. Events, sessions and registrations are a straightforward schema, and if that were the whole job the original estimate would have been right.
The complexity is that nearly every meaningful fact arrives from a system you do not control. Attendance lives in the video platform. Whether a reminder was delivered lives in the email platform. Segmentation lives in the marketing tool. Whether the event is on somebody's calendar depends on an ICS file being parsed correctly by a client you have never tested.
Each of those has its own model of a person, its own idea of time, and its own failure modes. The platform's real work is translation: keeping one coherent account of what happened while four external systems each report a partial and occasionally contradictory version of it.
That is why the architecture ends up with an explicit translation boundary rather than external calls scattered through the features. When the video platform changes what it returns, the change lands in one adapter instead of in every place attendance is touched. This is the same instinct as any anti corruption layer: external models do not get to dictate internal ones, because you will be living with your internal model long after the external one has been revised.
Attendance is not a boolean
Zoom knows who joined. It does not know who they are in your system, and the join records do not reliably line up with your registrations. People join from a second device. They join late from a phone with a display name their parent chose. They attend a session they never registered for because a colleague forwarded the link.
Automatic matching handles most of it. The decision that mattered was building a manual match path for the rest, rather than either guessing or dropping them. A certification is a claim someone will make about themselves later, possibly to an employer. It should not rest on fuzzy name matching being confident enough.
Reminders as scheduled work, not delayed jobs
Reminders go out at seven days, twenty-four hours and one hour. The naive version schedules three delayed jobs per registration at signup and hopes nothing changes.
Things change. Sessions move, people cancel, someone registers ninety minutes before a session starts and needs the one-hour reminder immediately or not at all. The reliable shape is a background process that periodically evaluates current state and asks what should be sent now, rather than a queue of promises made under conditions that no longer hold. It costs more to build and it survives a rescheduled event, which the delayed-job version does not.
Fifteen days, and the conversation about day sixteen
It shipped in about fifteen days as sole engineer and went into production for real educator cohort trainings, iterated under live testing first: timezone and ICS handling, mobile, and simplification of an admin UI that had grown to match the domain rather than the person using it.
The part worth writing down is what happened next. The work had clearly outgrown what was contracted, and there were two available responses: absorb it quietly, or say so. I raised it in writing and we agreed a time-boxed stabilisation window instead of an open-ended tail.
That is not a technical decision but it determined the outcome more than most of the technical ones. Scope creep on a fixed engagement is rarely a single conversation anybody refused to have. It is a series of small unremarked absorptions, each individually reasonable, and the project ends in a place neither side would have chosen deliberately. Naming it early is cheaper for the client than discovering it late, and it is the difference between a platform that gets finished and one that gets abandoned in a good-enough state.
What I would tell the version of me on day one
The estimate was not wrong about the event tool. It was wrong about how many systems had to agree for the event tool to be true. If a scope mentions attendance, certification and reminders in the same sentence, the build is an integration platform wearing a smaller job's clothes, and the honest estimate is the one that prices the translation layer rather than the schema.



Top comments (0)