UNPKG

atomic-saga

Version:

A comprehensive npm package for ensuring atomic API operations in distributed Node.js applications using Saga patterns, compensating transactions, and idempotent operations

413 lines (322 loc) โ€ข 17.2 kB
# atomic-saga ### โœจ Implement rock-solid, distributed transactions in Node.js with confidence. [](https://www.google.com/search?q=https://www.npmjs.com/package/atomic-saga) [](https://www.google.com/search?q=https://travis-ci.com/ankitsharma97/atomic-saga) [](https://www.google.com/search?q=https://github.com/ankitsharma97/atomic-saga/blob/main/LICENSE) [](https://www.google.com/search?q=https://www.npmjs.com/package/atomic-saga) Building reliable applications with microservices is hard. How do you handle a business transaction that spans multiple services, like processing a payment, updating inventory, and sending a confirmation email? If one step fails, how do you prevent leaving your system in an inconsistent state? **`atomic-saga`** provides a complete, production-ready toolkit to solve these challenges using proven architectural patterns. It empowers you to build resilient and reliable distributed systems on Node.js. ## ๐Ÿค” Why `atomic-saga`? - โœ… **Production-Ready Patterns**: Implements the Saga, Transactional Outbox, and Idempotency patterns right out of the box, saving you from building complex infrastructure from scratch. - ๐Ÿ‘จโ€๐Ÿ’ป **Developer-Friendly API**: A clean, fluent, and strongly-typed API makes defining, executing, and monitoring complex workflows straightforward. - ๐Ÿงฉ **Flexible & Extensible**: Bring your own storage and messaging systems. The package provides interfaces for databases (PostgreSQL, MongoDB) and message brokers (Kafka, RabbitMQ), allowing you to integrate with your existing stack. - ๐Ÿ›ก๏ธ **Built for Reliability**: With features like automatic rollbacks (compensations), configurable retries, and atomic event publishing, you can handle failures gracefully and ensure data consistency across services. ## ๐Ÿ“– Table of Contents - [Features](https://www.google.com/search?q=%23-features) - [Installation](https://www.google.com/search?q=%23-installation) - [Quick Start](https://www.google.com/search?q=%23-quick-start) - [Core Concepts](https://www.google.com/search?q=%23-core-concepts) - [Saga Pattern (Orchestration)](https://www.google.com/search?q=%23saga-pattern-orchestration) - [Compensating Transactions](https://www.google.com/search?q=%23compensating-transactions) - [Transactional Outbox](https://www.google.com/search?q=%23transactional-outbox-pattern) - [API Usage](https://www.google.com/search?q=%23-api-usage) - [Idempotent APIs](https://www.google.com/search?q=%23-idempotent-apis) - [Transactional Outbox](https://www.google.com/search?q=%23-transactional-outbox) - [Configuration](https://www.google.com/search?q=%23-configuration) - [Monitoring & Observability](https://www.google.com/search?q=%23-monitoring--observability) - [Store Implementations](https://www.google.com/search?q=%23%EF%B8%8F-store-implementations) - [Testing](https://www.google.com/search?q=%23-testing) - [Best Practices](https://www.google.com/search?q=%23-best-practices) - [Contributing](https://www.google.com/search?q=%23-contributing) - [License](https://www.google.com/search?q=%23-license) ## โœจ Features - **Saga Orchestration**: Define, execute, and monitor complex business workflows as a sequence of steps. - **Automatic Compensation**: If any step fails, the Saga automatically runs compensating actions to roll back previous steps. - **Configurable Retries**: Robust retry policies with exponential backoff for transient failures. - **Idempotent API Middleware**: A simple Express middleware to make your API endpoints idempotent, preventing duplicate operations. - **Transactional Outbox**: Guarantees that events are published if and only if the corresponding database transaction succeeds. - **Reliable Event Delivery**: A background processor ensures outbox messages are reliably delivered to your message broker. - **Full TypeScript Support**: Strongly typed from end to end for superior developer experience and fewer runtime errors. ## ๐Ÿ“ฆ Installation ```bash npm install atomic-saga ``` ## ๐Ÿš€ Quick Start ### 1\. Initialization First, create an instance of `AtomicApiOperations` with your chosen storage and logging implementations. For development, you can use the provided in-memory stores. ```typescript import { AtomicApiOperations, InMemoryIdempotencyStore, InMemorySagaStore } from 'atomic-saga'; // Use your preferred logger (e.g., Winston) const logger = console; // Configure the package with storage implementations const atomicApi = new AtomicApiOperations({ idempotencyStore: new InMemoryIdempotencyStore(), sagaStore: new InMemorySagaStore(), logger: logger }); ``` ### 2\. Define a Saga Define the steps of your business transaction. Each step has an `action` to perform the work and an optional `compensation` to undo it. ```typescript import { SagaDefinition } from 'atomic-saga'; const paymentSaga: SagaDefinition = { id: 'payment-processing', name: 'Payment Processing Saga', steps: [ { id: 'deduct-payment', name: 'Deduct Payment from Account', action: async (context) => { // 1. Call your payment service const result = await paymentService.deduct(context.amount, context.userId); return { transactionId: result.id }; // Pass output to the next step or compensation }, compensation: async (context, actionOutput) => { // 1a. If a later step fails, refund the payment await paymentService.refund(actionOutput.transactionId); }, retryPolicy: { maxAttempts: 3, backoffMs: 1000, backoffMultiplier: 2 } }, { id: 'update-inventory', name: 'Update Inventory', action: async (context) => { // 2. Call your inventory service await inventoryService.reserve(context.productId, context.quantity); return { inventoryReserved: true }; }, compensation: async (context) => { // 2a. Release the inventory reservation await inventoryService.release(context.productId, context.quantity); } }, { id: 'send-confirmation', name: 'Send Confirmation Email', action: async (context) => { // 3. Send a confirmation email await emailService.sendConfirmation(context.userId, context.orderId); return { emailSent: true }; } // No compensation needed, as we usually don't "un-send" an email. } ], onSuccess: async (context) => { logger.log(`Saga [${paymentSaga.id}] completed successfully for order ${context.orderId}.`); }, onFailure: async (context, error) => { logger.error(`Saga [${paymentSaga.id}] failed for order ${context.orderId}:`, error); } }; ``` ### 3\. Execute the Saga Run the Saga with the required initial data. The orchestrator will manage the entire flow. ```typescript async function processOrder() { const executionContext = { amount: 100.00, userId: 'user-123', productId: 'prod-456', quantity: 2, orderId: 'order-789' }; const result = await atomicApi.executeSaga(paymentSaga, executionContext); console.log('Saga execution finished with status:', result.status); // e.g., 'COMPLETED' or 'COMPENSATED' } ``` ## ๐Ÿ—๏ธ Core Concepts ### Saga Pattern (Orchestration) This package implements the **Saga Orchestration** pattern, where a central coordinator manages a distributed transaction. 1. **Start**: A client requests to start a Saga. 2. **Execute Step**: The orchestrator executes the first step's `action`. 3. **Continue**: If the step succeeds, it moves to the next one. 4. **Failure & Compensation**: If any step fails, the orchestrator executes the `compensation` function for all previously completed steps, in reverse order. 5. **State Management**: The state of the Saga is persisted, allowing it to be resumed after a crash. ### Compensating Transactions A compensating transaction is an operation that semantically reverses the effect of a previous step. It's the key to achieving "all or nothing" behavior. - **Action**: `paymentService.deduct(amount)` - **Compensation**: `paymentService.refund(transactionId)` Your compensation logic should be idempotent and designed to succeed even in failure scenarios. ### Transactional Outbox Pattern This pattern ensures atomic updates between a database and a message broker. It prevents a common distributed system failure: the database commit succeeds, but the message to notify other services fails to send. 1. **Atomic Operation**: The business data (e.g., `User` record) and the event (`UserUpdated` message) are saved to the database in the *same transaction*. The event is stored in a dedicated `outbox` table. 2. **Background Publisher**: A separate, reliable process polls the `outbox` table for unpublished events. 3. **Publish & Mark**: It publishes the events to a message broker (like Kafka or RabbitMQ) and, upon success, marks them as `published` in the database to prevent duplicates. ## ๐Ÿ”„ API Usage ### Idempotent APIs Protect your `POST` or `PUT` endpoints from duplicate requests caused by client retries or network issues. #### Express Middleware ```typescript import express from 'express'; import { AtomicApiOperations } from 'atomic-saga'; const app = express(); const atomicApi = new AtomicApiOperations(/* ... config ... */); // Add the middleware to your app. // It automatically checks for the 'X-Idempotency-Key' header. app.use(atomicApi.getIdempotencyMiddleware()); app.post('/api/payments', async (req, res) => { // This operation will now be idempotent. If a request with the same // idempotency key arrives, the middleware will return the cached response. const paymentResult = await processPayment(req.body); res.json(paymentResult); }); ``` #### Client Usage The client must generate a unique key and send it in the header. ```typescript // Generate a unique key for the operation const idempotencyKey = atomicApi.generateIdempotencyKey(); // Or use a UUID library // Send the key in the request header await fetch('/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Idempotency-Key': idempotencyKey, }, body: JSON.stringify(paymentData), }); ``` ### ๐Ÿ“จ Transactional Outbox Use the outbox to atomically save data and publish an event. ```typescript // 1. Configure AtomicApiOperations with an OutboxStore and MessageBroker const atomicApi = new AtomicApiOperations({ // ... other stores outboxStore: new PostgresOutboxStore(dbConnection), messageBroker: new KafkaMessageBroker(kafkaClient), }); // 2. Start the background processor (typically on app startup) atomicApi.startOutbox(); // 3. Use it in your business logic async function updateUser(userId: string, userData: any) { // Get a transaction handle from the outbox const outboxTransaction = atomicApi.createOutboxTransaction(); // Execute your database logic and define the event to be published await outboxTransaction.execute( // A function that contains your database updates async (transactionalClient) => { await database.updateUser(userId, userData, { client: transactionalClient }); await database.updateRelatedData(userId, { client: transactionalClient }); }, // The event to publish upon success { eventType: 'UserUpdated', payload: { userId, ...userData } } ); } ``` ## ๐Ÿ”ง Configuration ### `AtomicApiConfig` ```typescript interface AtomicApiConfig { idempotencyStore: IdempotencyStore; // Required: Manages idempotency keys. sagaStore: SagaStore; // Required: Persists Saga state. outboxStore?: OutboxStore; // Optional: Required for Transactional Outbox. messageBroker?: MessageBroker; // Optional: Required for Transactional Outbox. logger?: Logger; // Optional: For logging. Defaults to console. defaultRetryPolicy?: RetryPolicy; // Optional: Default retry policy for Saga steps. } ``` ### `RetryPolicy` ```typescript interface RetryPolicy { maxAttempts: number; // Maximum number of attempts for an action. backoffMs: number; // Initial delay in milliseconds. backoffMultiplier: number; // Factor to multiply delay by for each retry (e.g., 2 for exponential). } ``` ## ๐Ÿ“Š Monitoring & Observability ### Saga Execution Tracking ```typescript // Get details of a specific Saga execution const execution = await atomicApi.getSagaExecution('execution-id-123'); console.log('Execution Status:', execution.status); console.log('Step Results:', execution.stepResults); // List all completed executions for a specific Saga const executions = await atomicApi.listSagaExecutions({ sagaId: 'payment-processing', status: 'COMPLETED' }); ``` ### Outbox Statistics ```typescript const stats = await atomicApi.getOutboxStats(); console.log('Pending Events:', stats.pending); console.log('Published Events:', stats.published); console.log('Failed Events:', stats.failed); ``` ## ๐Ÿ› ๏ธ Store Implementations The package is unopinionated about your data storage. You provide the implementation by conforming to these interfaces. Example implementations for popular databases are planned for the future. - **`IdempotencyStore`**: - `set(key, response)` - `get(key)` - *Recommended Backend: Redis (for its speed and TTL support)* - **`SagaStore`**: - `createExecution(data)` - `getExecution(id)` - `updateExecution(id, data)` - *Recommended Backend: PostgreSQL, MongoDB* - **`OutboxStore`**: - `add(event, transaction)` - `getUnpublished()` - `markAsPublished(eventId)` - *Recommended Backend: PostgreSQL, MongoDB* ## ๐Ÿงช Testing The package is designed to be easily testable. Use the `InMemory` stores to test your Saga logic without external dependencies. ```typescript import { AtomicApiOperations, InMemorySagaStore, InMemoryIdempotencyStore } from 'atomic-saga'; describe('Payment Saga', () => { let atomicApi: AtomicApiOperations; let mockPaymentService; beforeEach(() => { atomicApi = new AtomicApiOperations({ idempotencyStore: new InMemoryIdempotencyStore(), sagaStore: new InMemorySagaStore(), }); // Mock external services mockPaymentService = { deduct: jest.fn().mockResolvedValue({ id: 'txn-123' }), refund: jest.fn().mockResolvedValue(true), }; }); it('should complete successfully when all steps succeed', async () => { // ... setup mocks to succeed const result = await atomicApi.executeSaga(paymentSaga, paymentContext); expect(result.status).toBe('COMPLETED'); expect(mockPaymentService.refund).not.toHaveBeenCalled(); }); it('should compensate successfully when a step fails', async () => { // Mock the inventory service to throw an error mockInventoryService.reserve.mockRejectedValue(new Error('Inventory not available')); const result = await atomicApi.executeSaga(paymentSaga, paymentContext); expect(result.status).toBe('COMPENSATED'); expect(mockPaymentService.refund).toHaveBeenCalledWith('txn-123'); }); }); ``` ## ๐Ÿ”’ Best Practices - **Design Idempotent Compensations**: Your compensation logic might be retried. Ensure it can be run multiple times without causing issues (e.g., don't refund twice). - **Keep Steps Small and Focused**: Each step should ideally interact with a single service or transactional boundary. - **Avoid Business Logic in the Orchestrator**: The Saga definition should only coordinate the steps. The actual business logic belongs in your services. - **Monitor for Failed Compensations**: A failed compensation is a critical error that requires manual intervention. Set up alerts to detect these scenarios. ## ๐Ÿค Contributing Contributions are welcome\! Please feel free to fork the repository, create a feature branch, and submit a pull request. 1. Fork the repository. 2. Create your feature branch (`git checkout -b feature/amazing-feature`). 3. Commit your changes (`git commit -m 'Add some amazing feature'`). 4. Push to the branch (`git push origin feature/amazing-feature`). 5. Open a pull request. ## ๐Ÿ™ Acknowledgments This package is heavily inspired by the patterns and principles from giants in the field: - "Saga" by Hector Garcia-Molina and Kenneth Salem - "Patterns of Enterprise Application Architecture" by Martin Fowler - "Building Microservices" by Sam Newman ## ๐Ÿ“„ License This project is licensed under the MIT License - see the [LICENSE](https://www.google.com/search?q=LICENSE) file for details. ----- For questions, issues, or contributions: "description": "A comprehensive npm package for ensuring atomic API operations in distributed Node.js applications using Saga patterns, compensating transactions, and idempotent operations", - GitHub Issues: [Create an issue](https://github.com/ankitsharma97/atomic-saga/issues) - Documentation: [Read the docs](https://github.com/ankitsharma97/atomic-saga/wiki) - Examples: [View examples](https://github.com/ankitsharma97/atomic-sagag/examples)