Google Calendar Two-Way Sync: OAuth2, Webhooks, and Distributed State

Architectural Takeaways
- Treat two-way synchronization as distributed state reconciliation
- Prevent sync loops by explicitly tracking change provenance
- Webhooks are notification signals, not complete payload state
- Asynchronous queues guarantee idempotency and retries
Integrating Google Calendar looks simple from the outside.
The user connects their Google account. The application creates a calendar event. Done.
That model breaks down as soon as the application needs two-way synchronization. Now there are two systems changing state independently:
Our Platform <===> Google Calendar
A user can create or modify an event from either side. That creates a distributed-state problem. For an EdTech platform with scheduled cohorts and learning sessions, calendar synchronization needed to be treated as an integration architecture rather than a simple API call.
1. The Goal
The platform needed to synchronize scheduling information between the application and Google Calendar. A simplified requirement was:
Platform creates session ──> Google Calendar event created
But the reverse also matters:
User modifies Google Calendar event ──> Platform eventually receives the change
That creates a two-way relationship where neither system can assume it is the only source of change:
┌─────────────────┐
│ │
▼ │
Our Backend <=======> Google Calendar
│ ▲
└─────────────────┘2. OAuth2 Authorization
The first problem is authorization. The application should not ask users for their Google password. Instead, OAuth2 allows the user to authorize the application to access the required Google Calendar resources.
The conceptual flow is:
User ──> Our Application ──> Google Authorization ──> User Grants Permission ──> Authorization Code ──> Backend ──> Access + Refresh Tokens
The backend can then make authorized Google Calendar API requests.
3. Access Tokens vs Refresh Tokens
OAuth2 introduces an important distinction:
- Access Token: Used to make API requests. It is temporary and short-lived.
- Refresh Token: Used to obtain a new access token after the access token expires.
Refresh Token ──> Google OAuth ──> New Access Token ──> Calendar API
This means the backend needs to manage token lifecycle correctly. A calendar integration that works once and then fails after token expiration is not a production integration.
4. Store Tokens Securely
OAuth credentials are sensitive. They should not be hardcoded, committed to Git, returned to the frontend, or logged. They should be stored using appropriate encrypted storage and protected by strict backend access controls.
The frontend generally does not need the Google refresh token. The backend owns the integration.
5. Creating an Event & Mapping Identifiers
Suppose a cohort session is scheduled:
The platform can create a corresponding Google Calendar event:
Cohort Service ──> Calendar Integration ──> Google Calendar API ──> Calendar Event
The critical mistake would be treating the Google event as the only record. The platform needs its own representation of the synchronization state.
6. Distributed State Mapping
Consider the mapping of identifiers:
Platform Event ID: 12345 <===> Google Event ID: abc987
This mapping allows future updates to target the correct external event. A synchronization record in PostgreSQL conceptually contains:
{
"internal_event_id": "12345",
"external_event_id": "abc987",
"user_id": "usr_9981",
"calendar_id": "primary",
"sync_status": "SYNCED",
"last_synced_at": "2026-08-30T16:42:00Z"
}External systems should not replace your application's source of truth.
7. Why Webhooks Matter
Polling Google Calendar repeatedly is inefficient and hits rate limits. The alternative is event-driven synchronization. Google Calendar provides push notifications through watch channels.
Google Calendar (Change) ──> Notification ──> Our Webhook ──> Sync Processor ──> PostgreSQL
The backend does not need to constantly ask "Did anything change?". Instead, Google tells the backend that something changed.
8. But a Webhook Is Not the Event
This is a subtle but important point. A push notification does not necessarily contain the complete changed calendar object. It acts as a signal: "Something changed."
The backend then retrieves the relevant calendar data and determines what changed:
Google ──> Webhook Notification ──> Backend ──> Fetch Calendar State ──> Compare / Reconcile ──> Update Local State
This is fundamentally different from receiving the complete event inside the webhook payload.
9. Synchronization Is Reconciliation
This changed how I thought about the integration. The backend isn't simply:
Receive event ──> Update database
It is a reconciliation process:
This pattern appears throughout robust distributed systems.
10. The Two-Way Sync Loop Problem
Consider this sequence:
Platform (Creates event A) ──> Google Calendar (User edits event A) ──> Webhook ──> Backend (Local event updated)
Now suppose the backend also updates Google whenever the local record changes. We can accidentally create an infinite sync loop:
Platform Update ──> Google Update ──> Webhook ──> Platform Update ──> Google Update ──> Webhook (Loop)
11. Preventing Sync Loops
The system needs to distinguish between a LOCAL-originated change and an EXTERNAL-originated change.
When the platform intentionally updates Google, the resulting webhook notification should not blindly trigger another write back to Google.
A bidirectional synchronization system must know where a change originated.
12. Idempotency & Asynchronous Queue Processing
Webhooks can be retried by Google. Network failures can cause duplicate delivery. Workers can crash after performing an operation but before recording completion.
Therefore, synchronization operations need to be idempotent. Webhook #123 delivered 3 times should not create 3 duplicate events.
A webhook endpoint should generally avoid performing a large synchronization operation directly inside the HTTP request. Instead:
Google ──> Webhook ──> Validate ──> Queue / Kafka ──> Sync Worker ──> Google API ──> PostgreSQL
- Shorter webhook response times (<50ms)
- Automatic retry mechanism on failure
- Failure isolation & rate limit handling
- Controlled processing concurrency
13. The Resulting Architecture
By decoupling notification ingestion from worker execution and explicitly tracking state provenance, the end-to-end synchronization pipeline resolves race conditions and scales predictably under high load.
Interactive Topology Map & Sequence Tracer
Select a component node or trigger an event flow to trace real-time data execution paths.
Primary Functions: Immediately responds with HTTP `200 OK` to Google within 50ms, verifies `x-goog-channel-id` header validation signatures, and delegates payloads downstream.
Design Constraint: Performs ZERO long-running business logic or direct Google API queries during the webhook request lifecycle.