|
| 1 | +# 🧭 Dhanman Messaging Design: Acknowledgment Event Pattern |
| 2 | + |
| 3 | +## **Objective** |
| 4 | + |
| 5 | +Enable asynchronous, reliable communication between services using **event-based acknowledgments** instead of blocking request–response. |
| 6 | +Each service that initiates a command will later receive a **targeted acknowledgment event** confirming completion and returning necessary identifiers. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 🧩 **1. Design Philosophy** |
| 11 | + |
| 12 | +- **Fire-and-Forget Commands:** Sales, Purchase, Payroll, and Community send commands to Common or other shared modules. |
| 13 | +- **Targeted Acknowledgment Events:** The receiver (e.g., Common) publishes back an acknowledgment event containing IDs or status for the initiating entity. |
| 14 | +- **Point-to-Point Routing:** Each acknowledgment event is delivered only to its respective source service, avoiding fanout noise. |
| 15 | +- **Guaranteed Delivery:** All acknowledgment events use durable queues bound to a **direct exchange** with explicit routing keys. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## 🔁 **2. High-Level Flow Example** |
| 20 | + |
| 21 | +```mermaid |
| 22 | +sequenceDiagram |
| 23 | + participant Sales as Dhanman.Sales |
| 24 | + participant Common as Dhanman.Common |
| 25 | +
|
| 26 | + Sales->>Common: 🔶 CreateTransactionCommand (InvoiceId, Amount) |
| 27 | + Common->>CommonDB: 💾 Create Transaction |
| 28 | + Common-->>Sales: 🔵 TransactionPostedForInvoiceEvent (InvoiceId, TransactionId) |
| 29 | + Sales->>SalesDB: 🧾 Update Invoice (TransactionId, Posted = true) |
| 30 | +``` |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## 🧱 **3. Exchange and Queue Strategy** |
| 35 | + |
| 36 | +| **Exchange** | **Type** | **Publisher** | **Routing Key Pattern** | **Consumer Service** | **Queue Example** | |
| 37 | +| ------------------- | ---------- | ------------- | ----------------------- | -------------------- | ------------------------------------------------------------------ | |
| 38 | +| `common.ack.events` | **direct** | Common | `invoice.posted` | Sales | `sales.invoice_posted.queue` | |
| 39 | +| `common.ack.events` | **direct** | Common | `bill.posted` | Purchase | `purchase.bill_posted.queue` | |
| 40 | +| `common.ack.events` | **direct** | Common | `salary.posted` | Payroll | `payroll.salary_posted.queue` | |
| 41 | +| `common.ack.events` | **direct** | Common | `customer.created` | Sales / Community | `sales.customer_created.queue`, `community.customer_created.queue` | |
| 42 | +| `common.ack.events` | **direct** | Common | `vendor.created` | Purchase | `purchase.vendor_created.queue` | |
| 43 | +| `common.ack.events` | **direct** | Common | `employee.created` | Payroll | `payroll.employee_created.queue` | |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## 🧩 **4. Complete List of Acknowledgment Events** |
| 48 | + |
| 49 | +| **Origin Service (Sender)** | **Target Publisher (Responder)** | **Command Sent** | **Acknowledgment Event** | **Payload Fields (Suggested)** | **Consumer Queue** | |
| 50 | +| --------------------------- | -------------------------------- | ---------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------- | |
| 51 | +| **Sales** | Common | `CreateBasicCustomerCommand` | `CustomerCreatedForSalesEvent` | `CustomerId`, `CompanyId`, `OrganizationId`, `CorrelationId`, `CreatedOnUtc` | `sales.customer_created.queue` | |
| 52 | +| | Common | `CreateTransactionCommand` | `TransactionPostedForInvoiceEvent` | `InvoiceId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `sales.invoice_posted.queue` | |
| 53 | +| | Common | `CreateInvoicePaymentCommand` | `TransactionPostedForInvoicePaymentEvent` | `InvoicePaymentId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `sales.invoice_payment_posted.queue` | |
| 54 | +| **Purchase** | Common | `CreateBasicVendorCommand` | `VendorCreatedForPurchaseEvent` | `VendorId`, `CompanyId`, `OrganizationId`, `CreatedOnUtc` | `purchase.vendor_created.queue` | |
| 55 | +| | Common | `CreateTransactionCommand` | `TransactionPostedForBillEvent` | `BillId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `purchase.bill_posted.queue` | |
| 56 | +| | Common | `CreateBillPaymentCommand` | `TransactionPostedForBillPaymentEvent` | `BillPaymentId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `purchase.bill_payment_posted.queue` | |
| 57 | +| **Payroll** | Common | `CreateBasicEmployeeCommand` | `EmployeeCreatedForPayrollEvent` | `EmployeeId`, `CompanyId`, `OrganizationId`, `CreatedOnUtc` | `payroll.employee_created.queue` | |
| 58 | +| | Common | `CreateTransactionCommand` | `TransactionPostedForSalaryEvent` | `SalaryId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `payroll.salary_posted.queue` | |
| 59 | +| | Common | `CreateSalaryPaymentCommand` | `TransactionPostedForSalaryPaymentEvent` | `SalaryPaymentId`, `TransactionId`, `CompanyId`, `OrganizationId`, `PostedOnUtc` | `payroll.salary_payment_posted.queue` | |
| 60 | +| **Community** | Common | `CreateBasicCustomerCommand` | `CustomerCreatedForCommunityEvent` | `CustomerId`, `UnitId`, `CompanyId`, `OrganizationId`, `CreatedOnUtc` | `community.customer_created.queue` | |
| 61 | +| **Common (reverse ack)** | Community | (Triggered when Unit creates a Customer) | `CustomerLinkedToUnitEvent` | `UnitId`, `CustomerId`, `CompanyId`, `OrganizationId`, `LinkedOnUtc` | `community.customer_linked.queue` | |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## ⚙️ **5. MassTransit Configuration Example** |
| 66 | + |
| 67 | +### **Common.Api Publisher** |
| 68 | + |
| 69 | +```csharp |
| 70 | +await _eventPublisher.PublishAsync(new TransactionPostedForInvoiceEvent |
| 71 | +{ |
| 72 | + InvoiceId = command.InvoiceId, |
| 73 | + TransactionId = transaction.Id, |
| 74 | + CompanyId = command.CompanyId, |
| 75 | + OrganizationId = command.OrganizationId, |
| 76 | + PostedOnUtc = DateTime.UtcNow |
| 77 | +}, context => |
| 78 | +{ |
| 79 | + context.SetRoutingKey("invoice.posted"); |
| 80 | +}); |
| 81 | +``` |
| 82 | + |
| 83 | +### **Sales.Api Consumer** |
| 84 | + |
| 85 | +```csharp |
| 86 | +cfg.ReceiveEndpoint("sales.invoice_posted.queue", e => |
| 87 | +{ |
| 88 | + e.ConfigureConsumeTopology = false; |
| 89 | + e.Bind("common.ack.events", x => |
| 90 | + { |
| 91 | + x.RoutingKey = "invoice.posted"; |
| 92 | + }); |
| 93 | + e.ConfigureConsumer<TransactionPostedForInvoiceConsumer>(context); |
| 94 | +}); |
| 95 | +``` |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## 🧠 **6. Why Use Direct Exchange** |
| 100 | + |
| 101 | +✔ Only specific consumers receive relevant messages |
| 102 | +✔ Reduces unnecessary message traffic |
| 103 | +✔ Keeps event routing explicit and predictable |
| 104 | +✔ Easy to extend — just add a new routing key and queue binding for another event type |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## 🪶 **7. Schema Example (Event DTO)** |
| 109 | + |
| 110 | +```csharp |
| 111 | +public record TransactionPostedForInvoiceEvent( |
| 112 | + Guid InvoiceId, |
| 113 | + long TransactionId, |
| 114 | + Guid CompanyId, |
| 115 | + Guid OrganizationId, |
| 116 | + DateTime PostedOnUtc, |
| 117 | + Guid CorrelationId); |
| 118 | +``` |
| 119 | + |
| 120 | +All acknowledgment events follow a consistent convention: |
| 121 | + |
| 122 | +- Include **CorrelationId** from originating command |
| 123 | +- Include **EntityId** (InvoiceId, BillId, etc.) |
| 124 | +- Include **TransactionId** |
| 125 | +- Include **OrganizationId**, **CompanyId**, and **Timestamp** |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## 🧩 **8. Summary Checklist** |
| 130 | + |
| 131 | +| ✅ Goal | 🧩 Implementation | |
| 132 | +| -------------------------------- | ------------------------------------------------------------------ | |
| 133 | +| Maintain async non-blocking flow | Fire commands, listen for acknowledgment events | |
| 134 | +| Event naming consistency | Use `TransactionPostedForXEvent`, `CustomerCreatedForXEvent`, etc. | |
| 135 | +| Routing isolation | Use direct exchange with service-specific routing keys | |
| 136 | +| Observability | Include CorrelationId in every message | |
| 137 | +| Reliability | Use Outbox pattern for event publishing | |
| 138 | +| Maintainability | Group all acknowledgments under `common.ack.events` exchange | |
0 commit comments