Blog/Google Calendar Two-Way Sync
Distributed Systems & IntegrationsAugust 30, 202610 min read

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

Tamirat Fereja, Backend Systems Engineer
Tamirat Fereja
Backend Systems Engineer | Specializing in NestJS, Kafka & Distributed Systems

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:

Cohort: Backend Engineering
Date: September 10
Time: 10:00–12:00

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:

Interactive State Inspector
{
  "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:

Interactive Reconciliation WorkflowStep 1 of 5
1. Detect possible change
Webhook push header received from Google Watch Channel.

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.

Infinite Sync Loop Guard Simulator
Incoming Change Source:PLATFORM_API (Local Write)
Google Webhook Triggered:YES (External Notification)
Execution Outcome: Block Echo Outbound (Loop Prevented)
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.

Inspector: API Gateway / Webhook HandlerComponent Responsibility Profile

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.

OAuth2 & AuthHandles identity delegation, grant rotation, and access token refreshes.
Calendar APIHandles low-level REST requests, delta queries, and payload normalization.
Webhook IngestionLightweight HTTP endpoint enforcing fast ACKs, authentication, and queuing.
Sync WorkerAsynchronous process managing idempotency, loop guards, and DB transactions.