https://github.com/mcking-07/eventing-framework
an opinionated event-driven framework for building asynchronous systems on aws.
https://github.com/mcking-07/eventing-framework
ecs-framework event-driven-architecture microservice-framework
Last synced: 7 days ago
JSON representation
an opinionated event-driven framework for building asynchronous systems on aws.
- Host: GitHub
- URL: https://github.com/mcking-07/eventing-framework
- Owner: mcking-07
- License: mit
- Created: 2026-05-24T00:15:30.000Z (25 days ago)
- Default Branch: main
- Last Pushed: 2026-05-24T20:33:44.000Z (25 days ago)
- Last Synced: 2026-05-24T22:22:09.144Z (25 days ago)
- Topics: ecs-framework, event-driven-architecture, microservice-framework
- Language: TypeScript
- Homepage:
- Size: 64.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# eventing-framework
[](https://github.com/mcking-07/eventing-framework/actions)
[](https://www.npmjs.com/package/eventing-framework)
[](https://github.com/mcking-07/eventing-framework/blob/main/LICENSE)
[](https://aws.amazon.com)
an opinionated event-driven framework for building asynchronous systems on amazon web services.
eventing-framework connects services through a typed event bus built on sns topics, sqs queues, and s3 storage. define domain events, run handlers when they arrive, and emit new events from anywhere in your code. the framework handles publishing, polling, oversized payloads, and s3 reference resolution behind the scenes.
- typed domain events with per-service base classes
- transparent oversized payload routing (inline or s3 reference)
- error isolation - a failing handler doesn't block the rest of the batch
- zero dependencies beyond the aws sdk
eventing-framework lets your services communicate without coupling. publish what happened. subscribe to what matters.
## installation
```sh
npm install eventing-framework
```
requires node.js 20 or later.
## usage
### defining events
extend `DomainEvent` to define your events. use an intermediate base class to inject per-service metadata once.
```typescript
import { DomainEvent } from 'eventing-framework';
class OrderEvent> extends DomainEvent {
constructor(name: string, payload: PayloadType) {
super(name, { ...payload, app: 'order-service', category: 'order' });
}
}
class OrderPlaced extends OrderEvent<{ id: string; total: number }> {
constructor(payload: { id: string; total: number }) {
super('OrderPlaced', payload);
}
}
class OrderProcessed extends OrderEvent<{ id: string }> {
constructor(payload: { id: string }) {
super('OrderProcessed', payload);
}
}
```
### configuring
configure only what your service needs, topic and queue for the publishing side, queue and storage for the consuming side.
```typescript
import { Application } from 'eventing-framework';
const app = new Application({
name: 'order-service',
environment: 'development',
topic: {
arn: 'arn:aws:sns:us-east-1:123456789012:order-events',
config: { region: 'us-east-1' },
},
queue: {
url: 'https://sqs.us-east-1.amazonaws.com/123456789012/notification-queue',
config: { region: 'us-east-1' },
params: { MessageAttributeNames: ['All'], WaitTimeSeconds: 5 },
},
storage: {
bucket: 'eventing-reference-bucket',
config: { region: 'us-east-1' },
},
scheduler: { interval: 5_000 },
});
```
### consuming events
services consume events published by other services. the framework polls sqs, resolves the payload, and routes it to your handler.
use a typed event map for auto-typed payloads.
```typescript
type AppEvents = {
OrderPlaced: { id: string; total: number };
OrderProcessed: { id: string };
};
const app = new Application({ ... });
app.on('OrderPlaced', async (payload) => {
console.log(`order ${payload.id} for $${payload.total}`);
});
```
or use an explicit generic on a bare `Application()`.
```typescript
const app = new Application({ ... });
app.on<{ id: string; total: number }>('OrderPlaced', async (payload) => {
console.log(`order ${payload.id} for $${payload.total}`);
});
```
### publishing events
register the event, then emit from anywhere in your process.
```typescript
import { EventPublisher } from 'eventing-framework';
app.register('OrderProcessed');
EventPublisher.emit(new OrderProcessed({ id: '123' }));
```
### chaining events
a handler can emit the next event in the flow, the chain continues to downstream services without coordination.
```typescript
app.on('OrderPlaced', async (payload) => {
await processOrder(payload);
EventPublisher.emit(new OrderProcessed({ id: payload.id }));
});
```
### starting and stopping
```typescript
await app.start(); // begins polling on the configured interval
await app.stop(); // stops the scheduler, clears handlers and registrations
```
## examples
two working examples against localstack, no aws account needed. see [examples](examples/) for more details.
- **[single event](examples/simple/)** - a publisher and a consumer communicating through a single event type
- **[multi-step workflow](examples/advanced/)** - four services chained across three event types, with oversized payloads and error handling
## design decisions
### why sns + sqs + s3
sns handles fan-out to multiple queues. sqs provides at-least-once delivery with visibility timeouts for retry. s3 stores payloads that exceed sns's 256kb message size limit, the framework routes oversized payloads transparently, consumers resolve references without knowing the difference.
### why EventPublisher is static
events come from handlers, webhooks, scheduled jobs - anywhere. a static emitter avoids threading an `Application` reference through every layer of your code. `EventPublisher.emit()` fires locally, `register()` catches it and publishes to sns.
### why register() and on() are separate
`register()` controls what leaves the service. `on()` controls what enters. the boundary is explicit; consume without publishing, publish without consuming, or both.
errors in handlers are isolated, one failing handler doesn't block the rest of the batch. the failed message stays in sqs for retry.
## api reference
### Application
```typescript
class Application> {
constructor(config: ApplicationConfig)
register(event: string): void
on(event: string, handler: (payload: PayloadType) => void | Promise): void
on(event: Event, handler: (payload: Events[Event]) => void | Promise): void
start(): Promise
stop(): Promise
}
```
### ApplicationConfig
```typescript
type ApplicationConfig = {
name: string
environment: 'development' | 'staging' | 'production'
topic?: {
arn: string
config: SNSClientConfig
params?: Omit
}
queue?: {
url: string
config: SQSClientConfig
params?: Omit
}
storage?: {
bucket: string
config: S3ClientConfig
}
scheduler?: {
interval: number
}
}
```
all service configs are optional, configure only what your service needs.
### DomainEvent
```typescript
class DomainEvent {
readonly id: string // uuid v7
readonly name: string // event name
readonly payload: PayloadType
readonly timestamp: number // epoch ms
}
```
extend `DomainEvent` to define your events. use an intermediate base class to inject per-service metadata (`app`, `category`) once.
### EventPublisher
```typescript
class EventPublisher {
static emit(event: EventType): void
}
```
emits a domain event to local listeners registered via `Application.register()`. can be called from anywhere in your process.