Skip to main content

Overview

The User Service communicates with other services via Amazon EventBridge, a serverless event bus. This provides loose coupling, enabling services to react to user lifecycle events without direct dependencies.

Why EventBridge?

RequirementEventBridgeSQS
One event → multiple consumersNative fan-outRequires SNS+SQS or multiple queues
Event filteringRules with pattern matchingLimited (message attributes)
Schema managementSchema RegistryManual
Audit trailArchive and replayManual implementation
Cross-accountNative supportRequires configuration
SQS is ideal for point-to-point messaging or work queues. EventBridge is better for event-driven architectures where multiple services react to the same event.

Events Published

User Events

Event TypeTriggerPayload
user.createdPost-confirmation triggeruserId, email, timestamp
user.updatedProfile updateuserId, changedFields, timestamp
user.deletedAccount deletionuserId, timestamp
user.suspendedAdmin actionuserId, reason, timestamp
user.reactivatedAdmin actionuserId, timestamp

Email Events

Event TypeTriggerPayload
email.addedNew email addeduserId, emailId, email
email.verifiedVerification confirmeduserId, emailId, email
email.removedEmail deleteduserId, emailId, email
email.primary.changedPrimary email changeduserId, oldEmail, newEmail

Event Schema

All events follow a consistent envelope:
{
  "version": "1.0",
  "source": "user-service",
  "detail-type": "user.created",
  "time": "2026-01-21T10:30:00Z",
  "detail": {
    "userId": "abc-123-def",
    "email": "[email protected]",
    "metadata": {
      "correlationId": "req-xyz-789",
      "environment": "production"
    }
  }
}

Schema Fields

FieldTypeDescription
versionstringSchema version for evolution
sourcestringPublishing service identifier
detail-typestringEvent type (domain.action format)
timeISO8601Event timestamp
detailobjectEvent-specific payload
detail.metadata.correlationIdstringRequest tracing ID
Always include version in events. Consumers can handle multiple versions during migration periods.

Event Consumers

Who Subscribes to What?

ServiceEventsPurpose
Notificationuser.created, email.added, email.verified, email.primary.changedSend welcome emails, verification codes, confirmations
Bookinguser.deleted, user.suspendedCancel pending bookings, block new bookings
Billinguser.deleted, email.primary.changedFinal invoice, update billing contact
Analyticsuser.*, email.*Usage tracking, conversion funnels
Search Indexuser.updated, user.deletedKeep search index in sync

EventBridge Rules

{
  "Rule": "UserEventsToNotification",
  "EventPattern": {
    "source": ["user-service"],
    "detail-type": [
      "user.created",
      "email.added", 
      "email.verified",
      "email.primary.changed"
    ]
  },
  "Targets": [{
    "Arn": "arn:aws:lambda:...:notification-handler"
  }]
}

Events Consumed

The User Service also consumes events from other services:
EventSourceAction
cognito.user.confirmedCognito (via trigger)Create DynamoDB user record
billing.subscription.changedBilling ServiceUpdate user tier/status
admin.user.suspendAdmin ServiceSet user status to suspended

Hard Parts

Event Ordering

Problem: Events may arrive out of order. user.updated might arrive before user.created. Solution:
  1. Timestamps: Every event includes time. Consumers can discard stale events.
  2. Idempotent handlers: Operations should be safe to retry or reorder.
  3. Last-write-wins: For non-critical data, accept the latest timestamp.
const handleUserUpdate = async (event: UserUpdatedEvent) => {
  const existing = await getLocalUser(event.detail.userId);
  
  // Ignore if we have newer data
  if (existing && existing.updatedAt > event.time) {
    console.log('Ignoring stale event');
    return;
  }
  
  await updateLocalUser(event.detail);
};

Eventual Consistency

Problem: After user.deleted, other services may still show the user briefly. Solution:
  1. Accept it: Most use cases tolerate seconds of delay.
  2. Critical path queries: For operations that cannot tolerate stale data, query the source service directly.
const createBooking = async (userId: string) => {
  // Critical operation - verify user status directly
  const user = await userServiceClient.getUser(userId);
  
  if (user.status !== 'active') {
    throw new Error('Cannot create booking for inactive user');
  }
  
  // Proceed with booking
};

Failed Event Processing

Problem: Consumer fails to process an event (bug, downstream outage). Solution: Dead Letter Queue (DLQ) with alerting and replay capability. DLQ Configuration:
  • Max retries: 3 (with exponential backoff)
  • DLQ retention: 14 days
  • Alarm on DLQ depth > 0

Guaranteed Delivery

Problem: Lambda publishes to DynamoDB but EventBridge call fails. Event is lost. Solution: Transactional Outbox Pattern
1

Write to Outbox

In the same DynamoDB transaction, write the event to an outbox table
2

Async Publisher

Separate Lambda polls outbox (or uses DynamoDB Streams) and publishes to EventBridge
3

Delete from Outbox

After successful publish, delete from outbox
// In the main handler
await dynamodb.transactWrite({
  TransactItems: [
    {
      Put: {
        TableName: 'UserTable',
        Item: updatedUser
      }
    },
    {
      Put: {
        TableName: 'OutboxTable',
        Item: {
          PK: `EVENT#${eventId}`,
          eventType: 'user.updated',
          payload: JSON.stringify(eventPayload),
          createdAt: new Date().toISOString()
        }
      }
    }
  ]
});
The outbox pattern adds complexity. Only use it when event delivery is truly critical. For many use cases, at-least-once delivery with idempotent consumers is sufficient.

Schema Evolution

Adding Fields

New fields can be added without breaking consumers:
// v1.0
{ "userId": "123", "email": "[email protected]" }

// v1.1 - backwards compatible
{ "userId": "123", "email": "[email protected]", "tier": "premium" }

Changing Fields

For breaking changes:
  1. Increment version
  2. Publish both versions during transition
  3. Consumers upgrade to new version
  4. Stop publishing old version

EventBridge Schema Registry

Register schemas for documentation and validation:
{
  "openapi": "3.0.0",
  "info": {
    "title": "UserService.user.created",
    "version": "1.0"
  },
  "components": {
    "schemas": {
      "UserCreated": {
        "type": "object",
        "required": ["userId", "email"],
        "properties": {
          "userId": { "type": "string", "format": "uuid" },
          "email": { "type": "string", "format": "email" }
        }
      }
    }
  }
}