Google and Microsoft do not break integrations out of malice. They run enormous platforms on their own schedules, and from where you sit the result is the same: endpoints get deprecated, quotas tighten, auth flows grow new steps, and scope policies shift underneath you. If your feature code calls providers directly, every one of those changes lands as your incident.
The churn is the steady state
None of this churn is rare. Provider APIs retire versions on published timelines, change notification formats, adjust rate limits per user and per app, and rework consent screens. Each change arrives with a migration window and a reasonable explanation. Individually, each is a small task. Cumulatively, they are a permanent tax on every team that integrates directly, paid in sprints that were planned for product work.
The tax is also unevenly timed. Deprecations cluster around provider release cycles, not around your roadmap. The month you planned a launch is the month a migration deadline lands.
Permission reviews recur
Access to mail and calendar data is gated. Google requires a verification process for sensitive scopes, and its most restricted scopes carry an independent security assessment that recurs annually. Change your requested scopes and parts of the review start again. This is reasonable policy, and it is also a real cost: calendar time, engineering time, and money, every year, for as long as you hold the scopes.
Budget for reviews as an operating expense, not a launch task. Teams that treat verification as a one-time gate get surprised the following year, usually at the worst possible moment.
One adapter boundary absorbs the churn
The structural fix is old: put every provider call behind an internal interface, and let feature code speak only that interface. When a provider deprecates an endpoint, the change is one adapter file and its tests, not a search across the codebase for every place that built a request by hand. The boundary also gives you one place to put retries, rate limiting, and error mapping, instead of one per feature.
The discipline that matters is refusing exceptions. The first time feature code reaches around the adapter for "just one field," the boundary stops being a boundary. Enforce it with lint rules and module visibility, not with comments.
What "normalized" actually means
Normalization is not renaming fields. For messages, it means a stable identifier that survives moves, one definition of a thread even though the two providers disagree about what a thread is, consistent sender and recipient shapes, and one body representation with an explicit story for HTML and plain text.
For calendar events, it means one recurrence model with expansion handled for you, a single attendee response status enum, time zones resolved the same way in both directions, and one place to find the online meeting link regardless of which provider attached it.
Every one of those is an opinion. A normalization layer picks one meaning, maps both providers onto it, and documents what does not survive the mapping. Writing that down is the difference between an abstraction and a leak.
The boundary is the decision
Whether you build the layer or buy it, what matters is that the boundary exists and is enforced. Building it costs adapter work up front and review cycles forever. Buying it means trusting someone else's opinions about what a message and an event are. Horato exists because that boundary is worth being a product: one API over email, calendar, contacts, and tasks across Google and Microsoft, with the churn absorbed on the far side of it. Either way, decide deliberately. The default, feature code calling providers directly, is the one option that gets more expensive every quarter.