You sit down to debug a failing integration. The logs show a connection was established, data bytes exchanged, then nothing—silent failure. The other team says their system sent a valid response. Yours says the handshake never completed. Both sides are right, in their own dialect.
Legacy handshakes are everywhere, buried under years of patches. They're the polite nod before two systems start shouting data at each other. But when that nod means different things to each participant, the whole conversation falls apart. This isn't a TCP issue—it's a semantic gap.
The Handshake That Never Was
A Friday afternoon post‑mortem
The ticket landed at 4:47 PM. ‘Legacy import stalled – customer data missing.’ I had seen this before – the pattern was almost boring. Two systems, one built on a mainframe that predated most of the team, the other a modern cloud service that handled real-time bookings. The handshake between them had worked for years. Until it didn’t. A database admin had patched the old system’s SSL library because of a critical vulnerability. Nobody thought to check if the new library still spoke the same dialect of TLS as the cloud endpoint. The handshake completed – technically. But the payload serialization shifted by three bytes. Three bytes. That shifted the offset where the booking timestamp lived. The cloud service read the timestamp as a customer ID, the customer ID as a status flag, and everything downstream fell apart. Seventeen thousand records silently mapped to wrong accounts. The fix took four hours. The data cleanup took three weeks. The real cost? Trust. The business stopped believing any data coming out of that pipeline could be trusted without manual review.
The invisible protocol layer
Most teams think of a handshake as a simple hello. Client says ‘I support X, Y, Z.’ Server says ‘Great, let’s use Y.’ Communication starts. That sounds fine until you’re in a room where both sides wave at each other and neither can tell if the other really saw it. Legacy handshakes are worse – they’re often undocumented, patched over years, carrying assumptions that nobody wrote down. The old system expected a comma after the last field. The new one didn’t. The old system sent dates in MM/DD/YYYY; the cloud service parsed DD/MM/YYYY. Not a bug – a handshake drift that took months to surface because the dates that broke were only the ones under 13. February 3rd? Fine. December 4th? Crashed. The invisible layer is the one nobody monitors – the negotiation between two codebases that have never been in the same meeting.
When both sides think they’re right
The hardest part of these post‑mortems is the first thirty minutes. Both engineering teams point at their logs. ‘Our system sent the correct format – look.’ ‘Ours accepted the connection and returned a 200 – it’s your data.’ Both are right. Both are wrong. The old system’s developer added a non‑breaking space inside a JSON key name in 2018 because a character encoding issue made it render correctly in a UI tool. Nobody flagged it. That space became part of the contract. The cloud team built their parser against the published spec, which didn’t include the invisible character. The handshake completed. The data arrived. But the key name didn’t match. The parser silently dropped fields it didn’t recognize. No alert. No error. Just a slow bleed of missing information that looked like the user forgetting to fill in fields. We fixed this by adding a byte‑level comparison between what the legacy system sent and what the new system expected – not just a schema check, but an actual bit‑for‑bit echo. The first run caught twenty‑seven invisible differences. Twenty‑seven. And that was just the handshake.
‘Our logs show we sent the data. Their logs show they received it. Neither log shows the handshake was a lie.’
— engineering director, two weeks into a data reconciliation after a silent handshake failure
Worth flagging – this particular failure didn’t cause an outage. No smoke, no pager alerts, no frantic Slack calls. It caused a slow data corruption that was only caught when an analyst noticed quarterly revenue projections looked ‘weird.’ Weird is the worst alarm you can build. By the time the handshake mismatch was found, seven quarters of data had been affected. The organization had made decisions on that data. They had hired people based on those decisions. That hurts. The handshake that never was – not a dropped connection, but a conversation where both parties heard different words and nobody said ‘wait, that doesn’t make sense.’ The fix wasn’t better code. It was better monitoring of the handshake itself. A three‑byte diff checker. A log entry when the two systems disagree on dialect. Simple. Cheap. Installed the day after the post‑mortem. Why did it take seven quarters to build that? Because nobody thought the handshake could fail silently. Now we know.
What People Get Wrong About Handshake Basics
Encoding assumptions bite first
I once watched two senior engineers burn an entire afternoon debugging a handshake that looked perfect on paper. Both sides sent '0xAA 0xBB' as the opening sequence. Both sides logged 'handshake received'. Yet the connection refused to stabilise. The culprit? One system treated the bytes as big-endian integers; the other read them as little-endian. Their monitors showed identical hex dumps — the machines saw different numbers entirely. That sounds like a beginner mistake, but I see this exact pattern on teams that have been building integrations for a decade.
The catch is that encoding fights rarely announce themselves with error codes. You get intermittent failures, timeouts that come and go, logs that say 'invalid token' when the token is perfectly valid according to your local view. Most teams skip this: they assume UTF-8 is universal, that ASCII covers all bases, that an 'S' is always 0x53. Wrong. A legacy ERP system might still speak EBCDIC on some internal path. A mainframe handshake could pad strings with spaces — or trim them silently. The seam blows out not because the protocol is complex, but because both sides never agreed what a byte means.
Worth flagging — character encoding isn't the only invisible trap. Timestamp formats vary wildly between systems ('MM/DD/YYYY' vs 'DD-MM-YYYY' vs Julian dates). I've seen a healthcare handshake fail because one side expected microseconds and the other sent nanoseconds. The fix is brutal: you document every field's byte layout, then test with an actual hex dump comparison. Not a spec review. Not a whiteboard session. Real bytes on the wire.
Timeout semantics are not universal
What does 'timeout after 30 seconds' mean to your team? Probably: wait 30 seconds, then kill the connection. But to the legacy system on the other end, it might mean: start a 30-second timer after receiving the first packet, and if the full handshake isn't complete by then, reset the session — even if traffic is still flowing. That mismatch killed a payment gateway migration I consulted on. The new service sent handshake responses in four quick bursts; the old host counted from packet one and dropped the session at second 29, right before the final ACK arrived.
The tricky part is that most timeout documentation is ambiguous. I've seen specs that say 'response must arrive within T seconds' without clarifying from when. Start of transmission? Last byte sent? After the previous response? These gaps turn into production incidents at 3 AM. One team I worked with assumed a 10-second timeout meant round-trip latency; the other side assumed it meant total connection lifetime. The result — every fifth handshake failed, randomly, with no pattern the logs could explain.
Not yet convinced? Consider retry logic. A legacy system might treat a timeout as a permanent failure and blacklist the source IP. Your modern service sees a timeout, retries three times with exponential backoff, and each retry makes the blacklist worse. What you call resilience, the old host calls an attack. — observation from a fintech integration post-mortem
Flag this for business: shortcuts cost a day.
Flag this for business: shortcuts cost a day.
Half-duplex vs. full-duplex confusion
Most teams assume modern APIs are full-duplex — both sides can talk at once. Legacy systems, especially those from the 1980s and 1990s, often enforce strict half-duplex: you speak, I listen, then I speak, you listen. A turn-taking protocol, like two people on a CB radio saying 'over'. When a modern service fires off three messages in rapid succession, the legacy host hears garbage — or it ignores everything after the first message because it's still 'listening' and hasn't finished composing its reply.
This is where handshakes silently degrade. The modern side logs 'sent handshake — waiting for response'. The legacy side logs 'received handshake — processing — busy — can't respond yet'. Both are correct. Both feel violated. The fix isn't harder — it's slower. You insert artificial pauses, you wait for an explicit 'ready' signal before sending the next chunk, you accept that speed is not always an improvement. That hurts when your team is measured on latency. But a fast handshake that works 70% of the time is infinitely slower than a careful one that works every time.
Three Patterns That Actually Work
Versioned envelopes with fallback
You have an old SOAP service that expects a rigid XML header. The new system sends JSON. Two people, different dialects, one wire. The trick that actually works: wrap everything in a versioned envelope that both sides can peel open. I have seen teams solve this by prefixing every message with a single byte—0x01 means legacy XML, 0x02 means modern JSON. The receiver reads that byte first, then dispatches to the right parser. That sounds fine until someone deploys a consumer that can only read byte 0x01. What breaks? Everything. So you add a fallback: if the receiver doesn't recognize the version byte, it defaults to the legacy format.
Worth flagging—this pattern only works if your legacy system can tolerate an extra byte of overhead. Some mainframe integrations choke on unexpected bytes. Test that before you ship. The catch is that fallback paths are rarely exercised until production. One team I worked with shipped a versioned envelope that fell back to legacy XML—great in staging, but the legacy parser silently truncated any field longer than 128 characters. Returns spiked for three days. They fixed it by adding a checksum after the version byte. Not beautiful. But it kept two dialects talking.
Progressive capability negotiation
Don't try to agree on everything at once. That's the mistake—designing a single handshake that covers encryption, schema version, timeout settings, and routing metadata. Instead, do it in layers. First layer: "Can you hear me?" (ping/pong). Second: "What verbs do you support?" (list of endpoints). Third: "What format works?" Then negotiate the rest. Most teams skip this: they bundle all capability discovery into one bloated message. The legacy system can't parse it, so it replies with a 500. Everyone blames the handshake. But the real problem is asking too many questions at once.
A concrete example: a retail integration where the legacy backend only understood EDI 850 purchase orders. The new middleware sent a capability request listing JSON, XML, and CSV. The legacy system didn't even reply. Why? The request itself was JSON. The fix: send the first probe as a plain TCP flag—no payload, just a connection. If the legacy system responds, you know the link is alive. Then you send your capability list in the format it already understands. Three rounds, not one. That hurts latency a bit. But a slow handshake that succeeds beats a fast one that fails. The trade-off is complexity—you now have state machines for each leg. Document them ruthlessly.
Idempotent retry with exponential backoff
Handshakes fail. Wires drop. Timeouts hit. But the worst pattern is retrying every 500 milliseconds forever—that's how you bring down a legacy system that was already limping. The pattern that works: idempotent retry with exponential backoff. Each attempt carries a unique idempotency key, so the legacy system can safely process the same handshake request twice without creating duplicate sessions or double-booking resources. Start at 1 second, then 2, then 4, then 8—cap at 60 seconds. After five failures, escalate to a dead-letter queue. No infinite loops.
'We lost a weekend because our handshake retry loop created 14,000 orphan sessions on the mainframe. Exponential backoff killed the pattern in one deploy.'
— Senior engineer, logistics platform migration
The tricky bit is making the retry idempotent. If your handshake creates a temporary reservation, the legacy system must treat the same key as a no-op. That means storing the key on the legacy side—which older systems often can't do without a schema change. One alternative: derive the key from the request payload itself (a hash of timestamp + client ID). Then even if the request is duplicated, the payload hash matches, and the legacy system ignores the second hit. Not perfectly clean—hash collisions are rare but real. Worth the risk? In practice, yes. The alternative—no retry at all—means your handshake fails on the first network blip, and your entire integration sits dead until a human wakes up.
Why Teams Keep Reverting to Bad Habits
The comfort of polling
When a legacy handshake starts glitching, the first instinct is almost always the same: poll faster. I have watched teams double their query frequency, shorten intervals from five seconds to one, then to three hundred milliseconds—hoping that more requests will somehow force the old system to behave. It feels productive. The logs fill up, dashboards show activity, and someone can say “we tightened the loop.” That sounds fine until you realize you have turned a ten-year-old batch system into a reactive hostage. The old mainframe wasn’t built for thirty-six thousand requests per hour. What usually breaks first is the queue depth. The polling pile-up hides a deeper failure: you're masking latency jitter by turning up the volume. One team I worked with spent six months polling a COBOL endpoint every eight hundred milliseconds. They never asked if the data actually changed that often. It didn’t.
Synchronous waits when async is cheaper
The second bad habit is the synchronous wait. Under pressure—during a cutover, a migration, a compliance freeze—teams lock the handshake into a blocking call. “Just hold the connection open until the legacy side responds.” It works in staging. Three systems, zero load, perfect network. In production, one five-second delay cascades into a five-minute queue jam. The anti-pattern is seductive because it feels like control. You see the thread wait, the timeout, the exact failure point. But control is illusion when your upstream system processes records on a tape drive that pauses for two seconds every third read. The catch is: synchronous waits hide drift. When the response finally arrives, it may be stale, corrupt, or addressed to the wrong session entirely. I have debugged handshakes where the sync callback returned a twenty-minute-old status because the legacy system silently queued the reply behind a batch job. Nobody noticed until the audit failed.
“We rewrote the interface to be asynchronous in one afternoon. It took three weeks to convince the team to stop polling.”
— Lead integrator, state insurance system migration
Copy-paste inheritance of bugs
Then there is the copy-paste problem. Not just cargo-cult code, but the quiet inheritance of timing assumptions that should have died years ago. A developer pulls a handshake snippet from a ten-year-old wiki, pastes it into a new microservice, and the system works. For a while. Until the legacy clock skew drifts past three seconds and the handshake breaks silently. What makes this anti-pattern vicious is its invisibility. The bug isn’t in the logic—it’s in the implied contract. The original snippet assumed the legacy endpoint responded within two hundred milliseconds. That assumption was true in 2012. Today, the same endpoint runs on emulated hardware with a network hop through three firewalls. The handshake still completes. It just takes eight seconds. Most teams revert to this habit because they’re in a hurry and the snippet has always worked before. Wrong order. What worked before worked because the conditions were different. Copy-paste inheritance doesn’t duplicate code; it duplicates expired shelf lives. One bank I consulted for had a handshake routine that included a hardcoded sleep(1500) call from 2005. Nobody touched it because “it passes tests.”
Odd bit about process: the dull step fails first.
Odd bit about process: the dull step fails first.
The Hidden Cost of Drift
A byte-order bug only on leap seconds
I helped untangle a handshake failure that had lived in the codebase for four years. It triggered exactly once—on June 30, 2015, during the leap-second insertion. The legacy system wrote a 32-bit timestamp in big-endian; the new middleware read it as little-endian. All other days the byte order happened to produce the same value. Not that day. The downstream database committed a timestamp from 1972, which caused a compliance audit to fail three months later. The team spent two weeks bisecting logs before anyone thought to check endianness. A handshake that works 99.9997% of the time is not a handshake—it's a time bomb with a long fuse.
The catch is that drift like this never announces itself. You don't get a red alert reading "byte-order mismatch on leap second." You get a weird invoice, a rejected packet, a user complaint that nobody can reproduce. The handshake layer ossifies gradually—engineers stop questioning why a particular field is four bytes instead of two, because "it's always worked." That's exactly when the seam blows out.
Retry storms from a silent upgrade
A payment gateway I audited had three services sharing one handshake protocol. Someone upgraded the TLS library on the orchestrator. No breaking changes in the API—or so they thought. The new library sent a slightly larger ClientHello. The legacy acceptor, built in 2008, had a hard-coded buffer of 512 bytes for the initial handshake record. Anything larger caused an immediate reset. The orchestrator retried. The acceptor reset. Within ninety seconds, all three services were hammering each other in a retry storm that saturated the network link. The fix took thirty seconds (bump the buffer). Finding the cause took eleven hours and a packet capture from the backup switch.
That's the hidden cost: the drift lives in the unwritten assumptions. The original author assumed 512 bytes would never be exceeded. The team that upgraded the library assumed the handshake was pure protocol, not a fixed-size contract. Both were wrong. And because the failure looked like a network problem, nobody connected it to the handshake layer for three days.
Gradual ossification of the handshake layer
Worth flagging—the most dangerous drift isn't the catastrophic kind. It's the slow calcification that makes every change cost 3x more. New engineers learn by copying the handshake module without understanding it. Workarounds pile up: a field is repurposed to carry a flag the original designer never intended, a checksum is disabled because "it breaks integration tests," a timeout value is doubled year after year until it's 120 seconds for a message that should take 400 milliseconds.
We had nine versions of the handshake header. Nine. Nobody could tell me which one was authoritative, so we supported all of them.
— A biomedical equipment technician, clinical engineering
— Platform engineer, after migrating a payment system off a 2006 codebase
That engineer's team spent eight months untangling the versions. The drift started innocuously: a field was added for a new client, then another field was repurposed because the original one ran out of bits, then a third version reordered fields for "performance." Each change made sense in isolation. Together, they created a handshake that could negotiate with itself in nine different dialects—none of which matched the original spec. The debugging cost was never budgeted. It simply accumulated, like interest on a zero-coupon bond that nobody remembered buying.
When You Shouldn't Use a Custom Handshake
When REST is enough
The simplest handshake I ever killed lived behind a login endpoint. One team had wrapped a legacy SOAP service inside a custom TLS-negotiation layer that required seven round trips before the first byte of payload arrived. Seven. The original architect had read a blog post about 'defense in depth' and taken it as a personal challenge. The fix was embarrassing in its simplicity: plain HTTPS POST with a JSON body. Authentication lived in the Authorization header, payload validation happened at the proxy, and the whole thing shipped in three days. The team saved 400 milliseconds per request. Nobody missed the custom dance.
That sounds fine until your org has twenty microservices, each with its own bespoke handshake protocol. I have seen teams build a custom handshake because they might need mutual TLS next quarter. They never do. The handshake calcifies, nobody remembers why the third byte must be 0x03 specifically, and onboarding a new service takes a week instead of an hour. The trade-off is brutal: you traded future-proofing for present-day complexity, and the future never arrived. REST over standard HTTPS handles 90% of inter-service communication without inventing a new dialect nobody speaks.
gRPC as a handshake escape hatch
Sometimes the payloads are heavy, the schema fights back, and REST starts leaking bytes everywhere. That's when gRPC earns its keep. The protocol handles its own handshake—HTTP/2 framing, TLS negotiation, connection multiplexing—all inside a defined spec that has been battle-tested at Google scale. You don't write a custom negotiator. You define a .proto file, generate stubs, and move on. The catch: teams treat gRPC as a magic eraser for bad handshake design. It's not. If your legacy system expects a three-phase commit before exposing data, gRPC can't paper over a bad architectural decision—it can only clean up the transport layer.
Worth flagging—gRPC brings its own constraints. The protocol is binary, debugging requires tools like grpcurl, and some firewalls hate HTTP/2 trailers. But compared to maintaining a custom handshake that three people understand, those constraints feel like minor inconveniences. I watched a fintech team replace a 1,200-line handshake handler with forty lines of gRPC interceptor code. The old handshake had seven unit tests that all passed but tested the wrong invariants. The new interceptor had two tests that actually caught failures. Fewer lines, fewer bugs, faster onboarding.
'We spent six months polishing a handshake that did less than a standard TLS handshake with a single header extension.'
— Staff engineer, after migrating to gRPC
Reality check: name the process owner or stop.
Reality check: name the process owner or stop.
The cost of reinventing wheels
Custom handshakes look like craftsmanship. They feel like ownership. The reality is different. Every bespoke protocol introduces a surface area for bugs that standard protocols already solved: retry semantics, timeout handling, version negotiation, backpressure signals. HTTP/2 has a built-in GOAWAY frame that tells clients to stop sending. Your custom handshake likely firehoses errors into a log that nobody reads. I have seen teams spend two sprints debugging a race condition that only appeared when the handshake sequence arrived out of order. The fix was to use a standard library. The fix was always possible.
The hidden cost nobody accounts for is cognitive drain. Every developer joining the team must learn the handshake ritual. Documentation rots. The original author leaves. Suddenly the handshake is a black box with one test that passes and no clear owner. Plain HTTP or gRPC give you a contract that thousands of developers already understand. That shared knowledge is worth more than any performance optimization your handshake might claim. The hardest lesson is admitting the custom wheel is wobbly. The best fix is often replacing it with a round one somebody else already shipped.
Next time your team designs a handshake, ask one question: would this solve a problem that HTTP/2 or gRPC can't? If the answer is not immediate and concrete, stop. Use the standard. Your future self—and your on-call rotation—will thank you.
Open Questions & FAQ
How do you test a handshake you can’t reproduce locally?
You set up a staging environment that mirrors production’s clock skew, network jitteryness, and middleware quirks — and then you still miss the real problem. I have seen teams burn two weeks chasing a handshake failure that only happened when a specific load balancer in Frankfurt was running at 60% memory pressure. The fix? A record-and-replay harness. Capture real traffic on a Shadow port or via tcpdump, then feed those exact byte sequences into your parser. It isn't perfect — replay can’t simulate timing races — but it catches structural misreads that unit tests never will. Another trick: instrument the handshake with structured logs and raise an alert the moment any step exceeds its historical p95 latency. That won’t reproduce the flaw, but it’ll flag that the flaw is happening, live.
Should you use monotonic clocks for timeouts?
Yes — but only if you understand what monotonic actually protects you against. Monotonic clock breaks when a Leap Second or an NTP step adjustment suddenly warps your wall-clock. Wrong order. The minute you compute a timeout as now() + 5 seconds using clock_gettime(CLOCK_REALTIME), you risk a negative duration if the system clock jumps backward. That hurts. However — and here is the trade-off — monotonic clocks abstract away wall-clock semantics entirely, so you lose the ability to anchor handshake deadlines to a human-readable timestamp. If your ops team wants to say “this handshake must complete before 14:32 UTC,” you have to convert manually. I see teams compromise: use monotonic for timeout computation, but log a snapshot of both clocks at handshake start. That way debugging stays possible and timeouts stay reliable.
Is WebSocket upgrade worth the complexity?
For most legacy-on-legacy conversations — no. WebSocket handshake introduces an HTTP Upgrade round-trip (GET + 101 Switching Protocols), then a separate framing protocol on top. That’s two state machines where you used to have one. The catch: if your legacy system already speaks HTTP, the upgrade path can be simpler than inventing a new application-level heartbeat. I fixed a 2010-era message queue handshake by letting it fake a WebSocket upgrade it would never actually use for streaming — it just used the upgrade negotiation as a reliable connection-parameter exchange. The client sent version, capabilities, server responded with an agreed timeout. That worked because both sides already had HTTP parsers. You aren’t adding WebSockets for the data channel; you’re borrowing the handshake lifecycle. What usually breaks first is the HTTP proxy between them — strips the Upgrade header silently unless configured to allow it. Test that path end-to-end before you write a single frame parser. Most teams skip this.
“We spent a month tuning the upgrade handshake, only to find an old F5 proxy was dropping the 101 response entirely. The protocol was fine. The infrastructure wasn’t.”
— Lead engineer, post-mortem on a retail payment gateway
There is one scenario where you shouldn’t even start the WebSocket conversation: if your latency budget is under 50 milliseconds. The extra round-trip destroys deterministic timing. Stick to a raw TCP handshake with a fixed-length magic byte sequence. Boring. Reliable. Debuggable with a hex dump.
Next Steps: What to Try This Week
Audit your handshake log for silent failures
Go find the last dozen handshake errors in your system logs — not the obvious 500s, but the ones that resolved themselves after one retry. I have seen teams discover that thirty percent of their so-called “transient” failures were actually dialect mismatches the legacy side never reported. The legacy box sent a field in the wrong order, the modern side ignored it, and the log entry read “processing complete” with a null where an integer should live. That silence costs you more than the crash you can see. Pull the raw payloads. Compare five “successful” handshakes against five that required a retry. If the field boundaries differ between the two groups, you have found the real bug — the one nobody filed.
The catch? You need permission to touch production logs, and most teams dodge that because it feels like an audit. It's. The trade-off is blunt: find the silent failures now, or find them during a month-end reconciliation that takes four engineers three days to untangle.
Add capability negotiation to one endpoint
Pick one REST or gRPC endpoint that talks to the worst-behaved legacy system — the one that crashes when you send a new optional field. Now add a capability-negotiation handshake: ask first, “What version of the contract do you speak?” before sending data. Most teams skip this because they assume both sides agree on the schema. Assume nothing. I watched a shipping backend waste two weeks debugging a handshake where the legacy side silently dropped any JSON key longer than twelve characters — because that was a hardware limit from 2003 nobody documented.
What usually breaks first is the negotiation call itself: the legacy system might choke on an unfamiliar HTTP header. That's fine. Fail it gracefully — default to the oldest known dialect, log exactly which capability was rejected. One concrete experiment: add an Accept-Dialect header to one endpoint and see whether the legacy side ignores it, echoes it back, or explodes. You learn more from a controlled explosion than from a silent drift that surfaces in production at 3 AM on a Saturday.
Worth flagging — negotiation adds latency. If your handshake already takes 400 ms, another round trip might push past an internal timeout. Test with a stopwatch, not a theory.
“We added negotiation to one endpoint and found out the legacy system hadn't spoken the dialect we thought it did for eighteen months.”
— Platform engineer, during a post-mortem I was invited to observe
Document the dialect mismatch as a producer/consumer contract
Stop describing the handshake in architecture diagrams or wiki pages nobody reads. Write a single producer/consumer contract — plain text, five pages max — that shows what the modern side sends, what the legacy side expects, and where they disagree. Not a wishlist. The actual mismatch. Do it this week: print the last ten failed handshake payloads and annotate each field that arrived but was ignored, or was expected but missing. That document becomes your shared truth — ugly, specific, and immediately useful.
The tricky bit is keeping it honest. Contracts tend to slide into “what the system should do” rather than “what it actually does.” Force yourself to include the current broken behavior. A line like “legacy system truncates field address_line_2 to 20 chars — data loss occurs, known since 2022-09-14” hurts to write. That hurt is the signal you need. Next action: hand that contract to a new team member. If they can read it and reproduce the handshake failure within an hour, you have a working document. If they can't, rewrite it — simpler, more explicit, less polite.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!