Should your webhook API mandate RFC 3339 timestamps or accept each partner's own date format at the edge?
answer
- The accepted set is a published interface
- Widening is easy, narrowing breaks people
- Detection cannot work on ambiguous values
- Exceptions need an owner and an end date
- Count who genuinely cannot change
basics
~20 sPublish one required format and always emit it; accept deviations only as a short, explicitly enumerated per-partner list with fixtures and an end date. The cost of a permissive edge is not the parsing code, it is owning an unbounded accepted set forever.
solid answer
~50 sTreat the accepted timestamp format as a published contract, not an implementation detail. My default is: the API emits RFC 3339 with an offset, always and only, and the specification names that as the required input. Then accept a small, named exception per partner who genuinely cannot change — recorded in a registry with its own layout, its own fixture and an agreed migration date — rather than a general-purpose normaliser. The reason is asymmetric cost: a strict contract pushes a one-off conversion onto each sender, while a permissive edge concentrates a permanent, unbounded testing and support obligation on the team that owns the receiver. What I will not accept is format sniffing, because ambiguous values like `03/04/2024` are unfalsifiable. And the call is not mine alone: whoever owns partner relationships can legitimately overrule strictness for a specific partner, in exchange for owning the exception's expiry.
code
go · 9 linestype senderFormat struct {
Layout string // exact layout; never inferred
Zone *time.Location // used only when Layout carries no offset element
RetireBy time.Time // when this exception is scheduled to go away
}
var formats = map[string]senderFormat{
"default": {Layout: time.RFC3339},
}go deeper
You will not own this call, but know that the timestamp format an API accepts is part of its documented contract, and that adding a new accepted format is a promise that is hard to take back.
Be able to argue why format detection cannot be made reliable for ambiguous dates, and what a per-sender configuration entry should contain instead of a chain of fallbacks.
Show how you would implement the policy: strict emission, an explicit exception registry, a counter per legacy path, and fixtures that make any accepted format's meaning testable.
Own the tradeoff and the boundary of your authority: set the default, name who can overrule it for a partner, and make each override carry an owner, an expiry and the metric that ends it.
## The decision, stated properly The question is not "can Go parse this" — it can parse anything you can describe with a layout. The question is what your API **promises**, because the accepted set of timestamp formats is a published interface. Widening it is easy and permanent; narrowing it later breaks partners. There are two coherent positions and one incoherent one. **Strict.** The specification names one format — RFC 3339 with an explicit offset — and anything else is rejected with an error naming the expected shape. Every ambiguity disappears at the boundary. The conversion work is distributed to the senders, each of whom does it once. **Permissive edge.** The receiver accepts whatever partners send and normalises internally. Partners integrate faster and nobody argues about a date format during a sales cycle. The cost lands entirely on the receiving team, and it is not the parsing code — that is trivial — it is the permanent obligation to keep an unbounded set of formats parsing correctly, forever, with no way to know when a format is no longer used. **Incoherent: sniffing.** Detecting the format from the data cannot work where the data is genuinely ambiguous. `03/04/2024` satisfies both a month-first and a day-first layout, so any detector is guessing, and it guesses differently on the 3rd of the month than on the 25th. This is worse than being consistently wrong, because it is not reproducible. Rule it out on correctness grounds, not on taste. ## The position I would actually hold Strict on **output**, unconditionally. What the service emits is entirely within our control, costs nothing to standardise, and a single format simplifies every consumer. RFC 3339 with an offset, normalised to UTC, sub-second precision, no exceptions, no per-consumer rendering. Strict on **input by default**, with named exceptions. Each exception is a registry entry naming the partner, the exact layout, the zone assumed for zone-less values, a pinned fixture, an owner and an expiry date. That structure matters more than the count: it makes the exception set finite, reviewable, and deletable. An exception without an owner and a date is just a permanent widening with extra steps. ## How I decide a specific case - **How many senders cannot change, really?** "Cannot" usually means "would need a release from a team we do not control." Two of those is a registry; twenty is an argument that the contract was published too late. - **What does the format cost to support forever?** An unambiguous non-standard format (epoch millis, a day-first layout with a four-digit year) is a fixture and a named decoding type. An ambiguous one costs a support conversation every time a value looks wrong, indefinitely. - **Who bears the failure?** If a misparsed timestamp corrupts our data silently, we bear it and strictness wins. If it merely delays one partner's onboarding, that is a business tradeoff someone else should weigh. - **Is there a migration path?** Accepting a legacy format alongside the standard one, with a dated deprecation and a metric counting how often the legacy path fires, is a very different commitment from accepting it with no end. ## Who owns it The API owner owns the specification and the default. The integrations or partnerships function owns partner relationships and can legitimately overrule strictness for a named partner — but the price of that override is owning the exception: its fixture, its expiry, and the conversation that ends it. Writing that down at the moment of the exception is the whole trick; an override granted verbally becomes a permanent, unattributed feature within a quarter. ## Making the decision visible Whatever the call, it needs three artefacts or it will erode: 1. **The published contract** — the format, with a worked example including a non-UTC offset, and an explicit statement about zone-less values (rejected, or interpreted in a named zone). 2. **A fixture per accepted format**, pinned to a reference instant chosen to be unambiguous — a day above the 12th, a non-zero offset, a non-zero fraction — so no accepted layout can quietly change meaning. 3. **A metric per legacy format**, so the exception's expiry is a data-driven conversation rather than an argument. When the counter reaches zero for a quarter, delete the entry and the code. ## The failure to argue against The usual counter-argument is that being permissive is friendlier. It is friendlier at onboarding and hostile afterwards, because the partner who sends a format you never documented has no way to know when you accidentally change how you interpret it — and neither do you. The strict contract is the one that lets both sides tell whether they are correct.
- A large partner refuses to send RFC 3339 and the deal depends on it. What do you agree to?Accept their format as a named registry entry, not as a general relaxation: their exact layout, the zone assumed for zone-less values, a pinned fixture, a counter on that decoding path, and a retirement date owned by whoever agreed to the exception. That converts an unbounded promise into one testable, attributable item that can actually be removed later.
- How do you decide when a legacy format can finally be dropped?From the counter on its decoding path, not from memory. When it has recorded no traffic for an agreed window, announce removal on the schedule set when the exception was granted, then delete the entry, the type and its fixture together. Without that metric the conversation is two people guessing, and the exception survives indefinitely by default.
- Should the service ever render timestamps in a consumer's preferred format?No — output is the half of the contract you fully control, and per-consumer rendering multiplies the shapes you must keep correct for no benefit to you. Emit one format everywhere, normalised to UTC with an explicit offset. Presentation formatting belongs at the far end, in whatever renders the value for a human.
saying these in an interview costs you the question
- Proposes detecting the sender's format from the data
- Treats the accepted format set as an implementation detail
- Grants a format exception with no owner or end date
- Renders a different output format per consumer
- Argues permissiveness is always the friendlier choice