This project demonstrates a basic Transactional Outbox Pattern implementation using Node.js, SQLite, and Kafka. The application simulates a reliable process of storing and sending messages with an outbox strategy, ensuring data consistency and reliability even in case of errors.
- Node.js (v14+ recommended)
- Kafka (Ensure Kafka is running locally or specify your brokers in the code)
-
Install Dependencies
Run the following command to install the necessary packages:npm install
-
Start Kafka
Make sure Kafka is running locally onlocalhost:9092
or update the broker information inkafka.js
. -
Run the Application
Use the following command to start the application:npm start
The main application logic is in app.js
, which includes database setup, event handling, and Kafka integration. Key elements include:
-
Imports and Utility Functions:
- Imports necessary modules like
kleur
for console log colors,DatabaseSync
for SQLite interactions, and file system utilities. rmDBStorage()
: Removes the SQLite database file if it exists.
- Imports necessary modules like
-
Error Handling and Cleanup:
- Listeners are set up for
exit
,uncaughtException
, andunhandledRejection
events to log errors and remove the database file if necessary.
- Listeners are set up for
-
Database Setup:
- A new
db.sqlite
file is created if it doesn't already exist, and two tables are set up:users
: Stores user data withid
andname
.users_outbox
: Implements the outbox pattern, storing events withid
,aggregateId
,eventType
, andpayload
.
- A new
-
Data Insertion:
- Sample user data is inserted into the
users
table. - An event is inserted into
users_outbox
for the last data entry, simulating a transactional behavior.
- Sample user data is inserted into the
-
Kafka Communication:
listenAndReceivePacket()
is called to listen for Kafka messages on a specified topic.- A packet with user data is sent using
sendPacket()
. On success, the corresponding outbox entry inusers_outbox
is deleted; on failure, an error is logged, and a retry is attempted.
The kafka.js
file defines the Kafka producer and consumer:
-
Producer and Consumer Setup:
- A Kafka instance is configured with a client ID and broker.
- A producer and a consumer with a group ID are set up for message handling.
-
Functions:
sendPacket(topic, packet)
: Sends messages to a Kafka topic.listenAndReceivePacket(topic)
: Listens for messages and logs received packets.
The package.json
file includes:
-
Scripts:
start
: Runs the application with experimental SQLite support.
-
Dependencies:
kafkajs
for Kafka communication.kleur
for console output colorization.
-- Users Table
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT
);
-- Users Outbox Table (Outbox Pattern)
CREATE TABLE users_outbox (
id INTEGER PRIMARY KEY,
aggregateId INTEGER,
eventType TEXT,
payload TEXT
);
This setup helps ensure that events are reliably processed and stored, following the outbox pattern to maintain data integrity across the database and Kafka messaging system.
The application implements basic error handling and retry logic to ensure reliable message processing:
-
Error Logging and Database Cleanup:
- If any of the following events occur:
exit
,uncaughtException
, orunhandledRejection
, the application logs the error and removes thedb.sqlite
file to clean up any residual data.
- If any of the following events occur:
-
Retrying Failed Message Sends:
- If
sendPacket()
fails to send a message to Kafka, the error is logged, and a retry mechanism kicks in by selecting unsent events from theusers_outbox
table and attempting to send them again.
- If
This project exemplifies several best practices of the transactional outbox pattern:
- Atomicity: Ensures that user data and related events are inserted into the database in a single, reliable step.
- Idempotency: If a message fails to send, the application checks the
users_outbox
table for unsent events, preventing duplicates. - Eventual Consistency: By storing the event in
users_outbox
and then attempting to send it to Kafka, the application ensures eventual consistency between the database and the message queue.
To verify that the messages are processed, you can use a separate consumer or monitor the console logs to see the output of listenAndReceivePacket()
in real-time as it listens to the specified Kafka topic.
The following line in kafka.js
logs each message received:
await consumer.run({
eachMessage: async ({ message }) => console.log(kleur.red(`Received packet: ${message.value.toString()}`))
})
This codebase could be expanded with additional features for production use:
- Automatic Retry Limits: Set a retry limit to prevent infinite retries.
- Error Notification System: Send alerts or notifications if certain errors occur multiple times.
- Batch Processing: Implement batch inserts to the database and batch sends to Kafka for improved performance.
This project demonstrates a fundamental setup for a Transactional Outbox Pattern in Node.js, designed to ensure reliable message processing with Kafka while maintaining database consistency. This pattern is ideal for applications requiring guaranteed delivery of events from a local database to an external messaging system.