We got direct pushback recently: “code reuse is a central theme here — if we’re not fully sharing IM and email threading, we must.” Fair pressure, and mostly right, but “share it” turned out to need real investigation before any code moved, not just a search-and-replace onto a common interface.

Thread-key resolution — the part that decides which messages belong to the same conversation — turned out to be genuinely different between the two, not just differently named. Mail does a real RFC 5322 chain-walk through References/In-Reply-To/Message-ID headers, falling back to subject matching when a header’s missing. IM’s version is a one-liner: platform plus chat id, since chat protocols hand you a stable thread identity for free. Forcing one interface over both wouldn’t have shared a single line of actual logic — it would have shared a name.

What genuinely was duplicated: the request/response event shape both domains used to ask “give me this thread’s prior messages,” and the per-row rendering (sender, timestamp, content) each domain had separately reimplemented. Those became ThreadEntry and ThreadMessage in the shared events module, and ThreadEntryRenderer for the row markup — Mail’s collapsed, truncated inline strip and IM’s full, untruncated thread view both call the same renderer now, even though the chrome wrapping each one (a collapsible strip for Mail, a bar-and-back-link for IM) stayed separate on purpose, since those really are different interaction shapes for different apps.

Unifying the event type surfaced a real bug we wouldn’t have hit otherwise: two distinct Java types used to route themselves for free through Spring’s type-based event dispatch — Mail’s listener only ever saw Mail’s request, IM’s only ever saw IM’s. Collapse them into one shared type and suddenly both listeners see every request, mail and IM alike, each one ready to answer a question that wasn’t meant for it. Fixed with a plain discriminator field on the request (moduleContext, “mail” or “im”) each listener checks before doing anything — the same guard pattern an earlier shared event type in this codebase had already established, so at least the fix itself wasn’t a new idea.

One more real duplication turned up along the way, not part of the original ask: IM’s own logic for picking a display name for a message’s sender — a linked contact’s real name if there is one, falling back to whatever the chat platform calls them — had been copied three separate times, across the view renderer, the AI’s own analysis prompt, and the new storage mapping this work touched. The AI’s copy was quietly missing the contact-linking step the other two had, meaning its prompts never got a person’s real name even when PIM already knew it. One canonical version later, that gap closed as a side effect of work that wasn’t looking for it.