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)