Event-driven architecture with TypeScript
- 09 Jul 2025 |
- 02 Mins read
At Pangea, we built an event-driven architecture for our FX and payments platform. TypeScript's type system made it possible to build reliable, type-safe event handlers across multiple services.
Why Event-Driven?
Event-driven architecture provides:
- Loose coupling between services
- Scalability (services scale independently)
- Resilience (failures are isolated)
- Flexibility (easy to add new consumers)
The Architecture
We use:
- Pub/Sub (Google Cloud Pub/Sub) for messaging
- TypeScript for type safety
- Event schemas defined in shared packages
- Dead letter queues for failed events
Event Schema
// Shared event types
export interface PaymentCreatedEvent {
type: 'payment.created';
paymentId: string;
amount: Decimal;
currency: CurrencyCode;
timestamp: Date;
metadata: Record<string, unknown>;
}
export interface PaymentFailedEvent {
type: 'payment.failed';
paymentId: string;
reason: string;
timestamp: Date;
}
export type PaymentEvent = PaymentCreatedEvent | PaymentFailedEvent;
Event Publisher
import { PubSub } from '@google-cloud/pubsub';
class EventPublisher {
private pubsub: PubSub;
private topic: Topic;
constructor(topicName: string) {
this.pubsub = new PubSub();
this.topic = this.pubsub.topic(topicName);
}
async publish<T extends PaymentEvent>(event: T): Promise<void> {
const messageId = await this.topic.publishMessage({
json: event,
});
console.log(`Published ${event.type} with ID ${messageId}`);
}
}
Event Consumer
class PaymentEventHandler {
private subscription: Subscription;
constructor(subscriptionName: string) {
const pubsub = new PubSub();
this.subscription = pubsub.subscription(subscriptionName);
}
async start(): Promise<void> {
this.subscription.on('message', (message) => {
const event = message.json as PaymentEvent;
this.handleEvent(event);
message.ack();
});
}
private async handleEvent(event: PaymentEvent): Promise<void> {
switch (event.type) {
case 'payment.created':
await this.handlePaymentCreated(event);
break;
case 'payment.failed':
await this.handlePaymentFailed(event);
break;
default:
console.warn(`Unknown event type: ${(event as any).type}`);
}
}
private async handlePaymentCreated(event: PaymentCreatedEvent): Promise<void> {
// Process payment created event
}
private async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
// Handle payment failure
}
}
Benefits
- Type safety catches errors at compile time
- Easy to add new event types
- Clear contracts between services
- Better IDE support and autocomplete
Challenges
- Event ordering (use partitioning keys)
- Idempotency (handle duplicate events)
- Error handling (dead letter queues)
- Testing (mock event publishers)
Best Practices
- Version your event schemas
- Make events idempotent
- Use dead letter queues
- Monitor event processing
- Document event contracts
"Events are the contracts between services. Make them explicit and type-safe."