#OrderPlaced
Event Sourcing requires a mental model shift.

"But I need to DELETE something!".

Here's the reality: you don't delete events.

You record a NEW event that something was deleted/cancelled/revoked.

OrderPlaced → OrderCancelled

Both facts are true. History doesn't have an undo button.
October 6, 2025 at 8:24 AM
I like Double Indemnity a lot. But I've never owned it on physical media in any fashion

Time to correct that. In 4K

#Criterion #OrderPlaced
July 11, 2026 at 1:15 AM
Event Sourcing tests double as documentation.

Given: [OrderPlaced, ItemAdded]
When: RemoveItem
Then: [ItemRemoved]

This reads like a spec. It IS a spec.
Your tests become the living documentation of your business rules.
October 5, 2025 at 8:24 AM
Internal Events:
- CustomerAgeCalculated
- InventoryReserved
Private domain implementation. Can change anytime

External Events:
- OrderPlaced
- PaymentReceived
Published API. Version instead.

Exposing internals creates distributed coupling.

Keep them separate.
October 11, 2025 at 8:24 AM
Your Process Manager (thing coordinating multiple services) can ITSELF be Event Sourced!

Events IN: OrderPlaced, PaymentReceived
Events OUT: ReservationRequested, ShipmentScheduled

Orchestration with full history and testable like any aggregate

It's events all the way down
October 24, 2025 at 8:24 AM
An event is a fact something happened in your domain.

Named with past tense verbs: `CustomerRegistered` or `OrderPlaced`.

Events are immutable, which makes sense when you think about it because you can't change the past.

This principle is critical to Event Sourcing
September 30, 2025 at 8:24 AM
Common Event Sourcing mistake: multiple aggregates in one stream.

One stream = One aggregate

Customer[123] stream → CustomerRegistered, EmailChanged, AddressUpdated
Order[456] stream → OrderPlaced, ItemAdded, OrderShipped

Different streams. Different things.
October 10, 2025 at 8:24 AM
Year 1: OrderPlaced {orderId, customerId, items}
Year 2: Needs "shippingMethod"

But, old events?
1. Upcasting: transform old events on read
2. Weak schema: new fields optional
3. Multiple event types: OrderPlacedV2

Each works:
- Weak schema = simple
- Upcasting = control
October 22, 2025 at 8:24 AM
⚠️ #Rustlang Tip: Use the #[must_use] attribute to ensure important return values aren't ignored.

Result is already marked as must_use but other types can be too!
It's good to add #[must_use] when ignoring the return value of a function is almost always a bug.
December 4, 2024 at 5:16 PM
What we just sold, Order: 21613, UVDTF For Materials: Quantity: 1 SOLD, Wed, 03 Jun 2026 12:01:47, click to buy yours #OrderPlaced #PrintShop #CustomPrinting #PrintServices #OrderDetails
Order: 21613, UVDTF For Materials
Quantity: 1 SOLD, Wed, 03 Jun 2026 12:01:47
gpprint.co.uk
June 5, 2026 at 9:01 AM
I'm just starting to dive into event sourcing. In a distributed system using streams (I'm using NATS), how would you typically model request/response interactions with services (eg. add a OrderPlaced event, wait for the corresponding OrderProcessed event)?
October 24, 2025 at 8:34 AM
Delete the Event Switch: Runtime Dispatch with dynamic in C#
A `switch` over event types is dynamic dispatch. You just wrote it by hand, took on the maintenance yourself, and skipped the help the runtime was ready to give you. You've written this method. Every event-driven codebase has one: public Task HandleAsync(BaseEvent @event) => @event switch { OrderPlaced e => HandleAsync(e), PaymentFailed e => HandleAsync(e), InventoryReserved e => HandleAsync(e), _ => throw new NotSupportedException( $"No handler for '{@event.GetType().Name}'.") }; It works. It's also a lookup table you maintain by hand, keyed on runtime type, mapping each type to the method that should run. That's the exact job overload resolution already does for you. The compiler does it for free, at every call site, and it never forgets to add a case. Let me show you how to hand that job back to the runtime. ## The setup Say we have a small, closed hierarchy of domain events that we own: public abstract record BaseEvent; public sealed record OrderPlaced(Guid OrderId, decimal Total) : BaseEvent; public sealed record PaymentFailed(Guid OrderId, string Reason) : BaseEvent; public sealed record InventoryReserved(Guid OrderId, int Qty) : BaseEvent; A processor needs to route each event to the right handler. The switch above does it, but the pattern-match arm and the target method are saying the same thing twice: _"if it's an`OrderPlaced`, call the `OrderPlaced` overload."_ The type check _is_ the dispatch. We just wrote it out longhand. ## Let the runtime dispatch it Cast to `dynamic` at a single boundary, and let the C# runtime binder pick the overload based on the argument's actual runtime type: public sealed class EventProcessor { // The only boundary where the type is late-bound. public Task HandleAsync(BaseEvent @event) { ArgumentNullException.ThrowIfNull(@event); return Dispatch((dynamic)@event); } private Task Dispatch(OrderPlaced e) => // handle the order... Task.CompletedTask; private Task Dispatch(PaymentFailed e) => // compensate, alert, retry... Task.CompletedTask; private Task Dispatch(InventoryReserved e) => Task.CompletedTask; // Least-specific overload: the intentional fallback (more on this below). private Task Dispatch(BaseEvent e) => throw new NotSupportedException( $"No handler is registered for '{e.GetType().Name}'."); } The switch is gone. Adding a fourth event now means adding its record and **one** `Dispatch` overload. Nothing central to edit, nothing to forget. ### Why the boundary and the handlers have different names Small thing, but it matters. The public entry point is `HandleAsync` and the overload set is `Dispatch`. That's on purpose. If the boundary were also called `Dispatch(BaseEvent)`, it would clash with the fallback overload, because two methods with the same signature are a compile error, and `public` versus `private` doesn't break the tie. Giving the boundary its own name gets you out of that, and it reads better anyway. One method's job is _"enter the dispatch"_ and the other set's job is _"handle a specific event."_ ## `dynamic` removes the conditional, not the responsibility This is the part that makes or breaks the pattern. `dynamic` deletes your `if`/`switch`, but it does not delete the need to handle an event you never wrote a handler for. It only changes _how you find out_. With no fallback, an unrecognized type throws a `RuntimeBinderException` from deep inside the framework. That's a confusing error that doesn't even name your domain. So add the fallback on purpose: private Task Dispatch(BaseEvent e) => throw new NotSupportedException( $"No handler is registered for '{e.GetType().Name}'."); Here's why it works. Every event _is a_ `BaseEvent`, so the binder always has at least the fallback overload available, and it picks the **most-derived** one that fits: * Runtime type is `OrderPlaced`, so `Dispatch(OrderPlaced)` is more specific than `Dispatch(BaseEvent)`, and the specific handler wins. * Runtime type has no dedicated overload, so only `Dispatch(BaseEvent)` fits, and you get your `NotSupportedException`, thrown from your code, with a message you control. Same guarantee the `_` arm gave you in the switch, now written as an overload. ## How this actually behaves at runtime Two questions people always ask. **"Isn't`dynamic` slow?"** There's a one-time cost the first time a given runtime type flows through the boundary. The DLR builds a call site and works out the overload. That result gets cached per runtime type. After warm-up, dispatching another `OrderPlaced` is about as fast as a normal virtual call, and you're not paying reflection costs on every event. For most event pipelines this just doesn't matter. For a genuinely hot path chewing through millions of events a second, benchmark it against a `Dictionary<Type, Func<…>>` or a source generator before you commit. **"Does`dynamic` leak everywhere?"** No, and this is the discipline that keeps it sane. `dynamic` lives in exactly one expression: `(dynamic)@event`. Every `Dispatch` overload is ordinary, statically typed C#. They get full IntelliSense, full type checking, and they're easy to unit-test on their own. The late binding stays in one small, contained spot instead of spreading through the class. ## Sharp edges worth knowing A few things that bite if you're not ready for them: * **Null.** A null `dynamic` has no runtime type for the binder to work with. That's why the boundary guards with `ArgumentNullException.ThrowIfNull` before the cast. Skip it and you get a murky runtime error instead of a clear one. * **Return type.** The expression `Dispatch((dynamic)@event)` is itself typed `dynamic` at compile time, so returning it from a `Task`-returning method adds a runtime conversion to `Task`. Keep every overload returning `Task` (or a `Task` subtype). If a handler ever returns something you can't await, you find out at runtime, not at compile time. * **Tooling goes quiet.** "Find all references" on `Dispatch(OrderPlaced)` won't show it being called, because statically it isn't. IDE navigation and some analyzers lose the thread across the `dynamic` hop. You're trading a bit of compile-time visibility for a lot less churn. ## When this is the right tool, and when it isn't Use it when your event types are a **closed, trusted hierarchy that you own** : domain events, internal commands, a message set defined in your own assembly. That's the whole premise. The runtime is choosing among a fixed set of methods you wrote, so late binding is safe, and the fallback covers the "someone added a type and forgot a handler" case. Do **not** point it at arbitrary deserialized payloads or plugin-provided types without checking them first. Late-binding on untrusted input is a footgun. You'd be handing the overload resolver whatever type an attacker or a buggy producer put on the wire. Validate or whitelist the type, _then_ dispatch. ## What about a type-pattern switch? Fair question. The modern `switch` expression over type patterns is a perfectly good alternative: public Task HandleAsync(BaseEvent @event) => @event switch { OrderPlaced e => Dispatch(e), PaymentFailed e => Dispatch(e), _ => throw new NotSupportedException(/* ... */) }; It's explicit, statically visible, and pays no binder cost. But notice it's still a table you keep by hand. Adding an event means editing it, and the `_` discard means the compiler won't force you to. It has the same "you have to remember the fallback" property as the dynamic version, just spelled out. So it comes down to what you want to optimize for: * Reach for **`dynamic`** when you want adding an event to need _only_ a new overload, and you're fine trading some static visibility for that. * Reach for the **switch** when you want the dispatch table sitting in one readable place and fully visible to your tooling. Both need the deliberate unknown-event branch. Neither is wrong. ## Scaling it up: resolving handlers from DI The version so far keeps the handler bodies inside the processor. In real code, they usually live in their own classes, resolved from the container, so they can take their own dependencies. This is where `dynamic` really earns its keep, because the awkward part of doing this with DI is the exact thing the cast fixes. Define the usual generic handler interface: public interface IEventHandler<in TEvent> where TEvent : BaseEvent { Task HandleAsync(TEvent @event, CancellationToken ct); } Now the dispatcher swaps its overload set for a _single generic method_ , and lets `dynamic` pick the type argument instead of the overload: public sealed class EventDispatcher(IServiceScopeFactory scopeFactory) { // Same one boundary as before. public Task DispatchAsync(BaseEvent @event, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(@event); return Handle((dynamic)@event, ct); } private async Task Handle<TEvent>(TEvent @event, CancellationToken ct) where TEvent: BaseEvent { await using var scope = scopeFactory.CreateAsyncScope(); var handlers = scope.ServiceProvider .GetServices<IEventHandler<TEvent>>() .ToList(); if (handlers.Count == 0) throw new NotSupportedException( $"No handler registered for '{typeof(TEvent).Name}'."); foreach (var handler in handlers) await handler.HandleAsync(@event, ct); } } When `@event` is runtime-type `OrderPlaced`, the binder infers `TEvent = OrderPlaced` on that generic method, and `GetServices<IEventHandler<OrderPlaced>>()` resolves the correctly typed handlers. That's the whole point. `dynamic` is bridging _runtime type_ to _generic type parameter_ , the one gap you can't cross from a static `BaseEvent` reference. Registration is the plain generic story, and with assembly scanning (Scrutor), adding an event touches the dispatcher **zero** times: services.Scan(s => s .FromAssemblyOf<OrderPlaced>() .AddClasses(c => c.AssignableTo(typeof(IEventHandler<>))) .AsImplementedInterfaces() .WithScopedLifetime()); Two things change once DI is in the picture, and both are worth calling out. **The fallback moves.** In the overload version, an unknown event got caught by the most-derived overload that fit, and `Dispatch(BaseEvent)` threw. Here there's one generic method, so every subtype runs it happily and `GetServices` just hands back an empty sequence. The "fail loud on an unhandled event" behavior no longer comes for free, so you put it back with the empty-set check above. Same guarantee, different mechanism. It's a count check now, not overload resolution. **Lifetimes are the real trap.** Resolving handlers from the container here is fine, not a service-locator smell, because the handler type genuinely isn't known until runtime. That's the textbook case for it, and it's how MediatR dispatches internally. But _which_ provider you resolve from matters a lot. Event processors are usually long-lived, like a queue consumer or a `BackgroundService`, and resolving _scoped_ handlers off the root provider from a long-lived object is the classic captive-dependency bug that shows up later as an `ObjectDisposedException`. That's why the dispatcher takes `IServiceScopeFactory` and opens a fresh scope per event instead of hanging onto one provider for its whole life. If your dispatcher already lives inside a scope, say it's resolved per web request, you can inject the scoped `IServiceProvider` directly and skip the manual scope. One footnote for the reflection-minded. The pre-`dynamic` way to do all of this is `typeof(IEventHandler<>).MakeGenericType(@event.GetType())` followed by a reflected invoke, usually behind a cached compiled delegate. It's the same idea written out by hand. `dynamic` just lets the DLR do the `MakeGenericType`, the resolution, and the per-type call-site caching for you. Same trade as the rest of the post: you give up "find all references" on the handlers in exchange for never editing the dispatcher. ## The takeaway The switch was never wrong. It was redundant. Picking a method based on the runtime type of an argument is the whole definition of overload resolution, and the runtime already knows how to do it, faster and more reliably than a hand-kept list of `case` labels. `dynamic` just asks it to do that job one moment later than usual. Delete the switch, keep the fallback.
dev.to
August 13, 2026 at 4:48 AM
Testing Laravel Events and Listeners: Ensuring Reliable Asynchronous Workflows
In modern Laravel applications, events and listeners are the glue that holds our complex business logic together. They allow us to decouple our code, keeping controllers thin and services focused. However, as applications scale, this "decoupling" can become a testing nightmare. We’ve all been there. You trigger an OrderPlaced event. It’s supposed to send an email, update the inventory, and notify the warehouse. One day, you realize the email never sent because an exception in the inventory listener swallowed the entire process. If you aren’t testing your events properly, you aren’t just missing code coverage—you are leaving your business workflows to chance. In this guide, we’ll move beyond basic Event::fake() assertions and explore how to build a robust testing strategy for event-driven Laravel applications. Previous article in this category: https://codecraftdiary.com/2026/06/22/tdd-in-laravel/ ## The Problem with "Testing Too Much" When developers start testing events, the default instinct is to reach for Event::fake(). It’s easy, it’s fast, and it makes the test pass. public function test_order_is_placed(): void { Event::fake(); $this->post('/checkout', [...]); Event::assertDispatched(OrderPlaced::class); } This test tells us one thing: Did we trigger the event? It tells us absolutely nothing about whether the listeners actually work or if they communicate correctly with each other. If you rely solely on faking, your test suite becomes a "smoke screen" that passes even when your underlying infrastructure is broken. ## 1. Unit Testing Listeners in Isolation The best way to ensure reliability is to treat your Listeners like any other service class. A listener should have one job. If it has complex logic, extract it into a dedicated Action or Service class and test that. // app/Listeners/SendOrderConfirmation.php public function handle(OrderPlaced $event): void { // Don't put business logic here! $this->mailer->sendConfirmation($event->order); } By keeping the listener thin, your unit test for SendOrderConfirmation becomes trivial. You can mock the Mailer service and simply verify the handle() method is called correctly. This is fast, deterministic, and catches bugs in the communication layer without overhead. ## 2. Integration Testing: The "End-to-End" Event Flow When you want to verify that the OrderPlaced event actually triggers the SendOrderConfirmation listener and writes the correct data to the database, you need an integration test. Crucially, do not fake the event here. Instead, allow the event to dispatch and verify the side effects. public function test_order_placed_event_triggers_side_effects(): void { // 1. Arrange: Setup user and cart $order = Order::factory()->create(); // 2. Act: Trigger the event directly OrderPlaced::dispatch($order); // 3. Assert: Check side effects, not just the dispatch $this->assertDatabaseHas('emails', [ 'order_id' => $order->id, 'status' => 'sent' ]); } This approach proves that your Service Provider is correctly registered and that the binding between the Event and Listener is active. ## 3. Handling Asynchronous Queues The biggest trap is the mix of synchronous and asynchronous listeners. If your listener implements ShouldQueue, Event::fake() will prevent the job from ever being pushed to the queue. To test queued listeners, use the Bus and Queue facades in tandem: public function test_order_placed_event_queues_notification(): void { Queue::fake(); Event::dispatch(new OrderPlaced($order)); Queue::assertPushed(SendOrderConfirmation::class, function ($job) use ($order) { return $job->order->id === $order->id; }); } Pro Tip: If you are testing a complex flow, don't forget to test the failure state. Use Queue::assertPushed to verify that the retry logic or failure handling (like failed() methods) is configured for your mission-critical jobs. ## 4. Avoiding "The Silent Failure" One common mistake is neglecting what happens when a listener fails. If your application relies on a chain of events, you must test the "atomic" nature of the flow. If you are using Laravel 11/12+ features, ensure you are testing your custom shouldDiscoverEvents logic if you use event discovery. Hidden logic is the enemy of maintainable tests. If a developer adds a new listener to a directory, does your test suite automatically include it? By writing explicit integration tests for critical flows, you ensure that even "magically" discovered events are held accountable. ## Testing Checklist for Events To keep your test suite maintainable as your project grows: * Logic Extraction: If a listener is more than 5 lines, move the logic to an Action class. * Fake Sparingly: Use Event::fake() only when you specifically want to verify the triggering of an event, not its outcome. * Verify Side Effects: Always write at least one integration test that lets the event fire "for real" to ensure the pipeline is wired up correctly. * Queue Awareness: Always distinguish between testing the dispatch and testing the queued job execution.
dev.to
July 14, 2026 at 2:13 PM
Transactional Messaging in .NET: Integrating Brighter’s Outbox Pattern with SQL Server and RabbitMQ
## Introduction In the last article, we explored the outbox pattern and a generic way to configure it. This time, we’ll dive into implementing the Outbox Pattern with SQL Server to guarantee transactional consistency between database updates and message publishing. ## Project The main idea of this project is to send a command to create an order, when the order is create, it'll send 2 messages `OrderPlaced` & `OrderPaid`, in case we have a failure, we shouldn't send any message. ### Requirement * .NET 8+ * Podman (or Docker) to run local containers: * SQL Server * RabbitMQ * Brighter knowledge about RabbitMQ * Nuget packages * Paramore.Brighter.Extensions.DependencyInjection * Paramore.Brighter.Extensions.Hosting * Paramore.Brighter.MessagingGateway.RMQ * Paramore.Brighter.Outbox.MsSql * Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection * Paramore.Brighter.ServiceActivator.Extensions.Hosting ### Messages For this project we will need these 3 message: `CreateNewOrder`, `OrderPlaced` and `OrderPaid` public class CreateNewOrder() : Command(Guid.NewGuid()) { public decimal Value { get; set; } } public class OrderPlaced() : Event(Guid.NewGuid()) { public string OrderId { get; set; } = string.Empty; public decimal Value { get; set; } } public class OrderPaid() : Event(Guid.NewGuid()) { public string OrderId { get; set; } = string.Empty; } ### Message Mappers Since only `OrderPlaced` and `OrderPaid` events are published to RabbitMQ, we need to implement mappers for them using JSON serialization public class OrderPlacedMapper : IAmAMessageMapper<OrderPlaced> { public Message MapToMessage(OrderPlaced request) { var header = new MessageHeader(); header.Id = request.Id; header.TimeStamp = DateTime.UtcNow; header.Topic = "order-placed"; header.MessageType = MessageType.MT_EVENT; var body = new MessageBody(JsonSerializer.Serialize(request)); return new Message(header, body); } public OrderPlaced MapToRequest(Message message) { return JsonSerializer.Deserialize<OrderPlaced>(message.Body.Bytes)!; } } public class OrderPaidMapper : IAmAMessageMapper<OrderPaid> { public Message MapToMessage(OrderPaid request) { var header = new MessageHeader(); header.Id = request.Id; header.TimeStamp = DateTime.UtcNow; header.Topic = "order-paid"; header.MessageType = MessageType.MT_EVENT; var body = new MessageBody(JsonSerializer.Serialize(request)); return new Message(header, body); } public OrderPaid MapToRequest(Message message) { return JsonSerializer.Deserialize<OrderPaid>(message.Body.Bytes)!; } } ## Request Handlers For `OrderPlaced` and `OrderPaid` we are going to log the received message. public class OrderPlaceHandler(ILogger<OrderPlaceHandler> logger) : RequestHandlerAsync<OrderPlaced> { public override Task<OrderPlaced> HandleAsync(OrderPlaced command, CancellationToken cancellationToken = default) { logger.LogInformation("{OrderId} placed with value {OrderValue}", command.OrderId, command.Value); return base.HandleAsync(command, cancellationToken); } } public class OrderPaidHandler(ILogger<OrderPaidHandler> logger) : RequestHandlerAsync<OrderPaid> { public override Task<OrderPaid> HandleAsync(OrderPaid command, CancellationToken cancellationToken = default) { logger.LogInformation("{OrderId} paid", command.OrderId); return base.HandleAsync(command, cancellationToken); } } #### Create New Order The `CreateNewOrder` handler is going to wait for 10ms to emulate a process, then publish the `OrderPlaced`, if the value is mod 3 throw an exception (emulation a business error), otherwise publish `OrderPaid`. public class CreateNewOrderHandler(IAmACommandProcessor commandProcessor, IUnitOfWork unitOfWork, ILogger<CreateNewOrderHandler> logger) : RequestHandlerAsync<CreateNewOrder> { public override async Task<CreateNewOrder> HandleAsync(CreateNewOrder command, CancellationToken cancellationToken = default) { await unitOfWork.BeginTransactionAsync(cancellationToken); try { string id = Guid.NewGuid().ToString(); logger.LogInformation("Creating a new order: {OrderId}", id); await Task.Delay(10, cancellationToken); // emulating an process _ = await commandProcessor.DepositPostAsync(new OrderPlaced { OrderId = id, Value = command.Value }, cancellationToken: cancellationToken); if (command.Value % 3 == 0) { throw new InvalidOperationException("invalid value"); } _ = await commandProcessor.DepositPostAsync(new OrderPaid { OrderId = id }, cancellationToken: cancellationToken); await unitOfWork.CommitAsync(cancellationToken); return await base.HandleAsync(command, cancellationToken); } catch { logger.LogError("Invalid data"); await unitOfWork.RollbackAsync(cancellationToken); throw; } } } **Key Insight:** * `IUnitOfWork` shares Brighter's SQL transaction to ensure atomicity (order persistence + outbox writes). * Events are only published if the transaction commits. ### Configuring Microsoft SQL Server To integrate the Outbox Pattern with SQL Server, first ensure the `OutboxMessages` table exists. #### 1. SQL Table Schema IF OBJECT_ID('OutboxMessages', 'U') IS NULL BEGIN CREATE TABLE [OutboxMessages] ( [Id] [BIGINT] NOT NULL IDENTITY, [MessageId] UNIQUEIDENTIFIER NOT NULL, [Topic] NVARCHAR(255) NULL, [MessageType] NVARCHAR(32) NULL, [Timestamp] DATETIME NULL, [CorrelationId] UNIQUEIDENTIFIER NULL, [ReplyTo] NVARCHAR(255) NULL, [ContentType] NVARCHAR(128) NULL, [Dispatched] DATETIME NULL, [HeaderBag] NTEXT NULL , [Body] NTEXT NULL, PRIMARY KEY ( [Id] ) ); END #### 2. Dependency Injection Setup Register the outbox and transaction. services .AddServiceActivator(opt => { // Subscription setup (see previous article) }) .UseMsSqlOutbox(new MsSqlConfiguration(ConnectionString, "OutboxMessages"), typeof(SqlConnectionProvider), ServiceLifetime.Scoped) .UseMsSqlTransactionConnectionProvider(typeof(SqlConnectionProvider)) .UseOutboxSweeper(opt => opt.BatchSize = 10); **Why This Works:** * `UseMsSqlOutbox` links the outbox to SQL Server. * `UseOutboxSweeper` configures background polling for undelivered messages. #### 3. Transaction Management To ensure atomicity between business logic and message publishing in Brighter, implement `IMsSqlTransactionConnectionProvider` and `IUnitOfWork` for shared transaction context. This guarantees that messages are only stored in the outbox if the database transaction commits successfully. ##### a. SqlConnectionProvider public class SqlConnectionProvider(SqlUnitOfWork sqlConnection) : IMsSqlTransactionConnectionProvider { private readonly SqlUnitOfWork _sqlConnection = sqlConnection; public SqlConnection GetConnection() { return _sqlConnection.Connection; } public Task<SqlConnection> GetConnectionAsync(CancellationToken cancellationToken = default) { return Task.FromResult(_sqlConnection.Connection); } public SqlTransaction? GetTransaction() { return _sqlConnection.Transaction; } public bool HasOpenTransaction => _sqlConnection.Transaction != null; public bool IsSharedConnection => true; } ##### b. Unit of work And finally we need to create a new interface and implement an interface called `IUnitOfWork` public interface IUnitOfWork { Task BeginTransactionAsync(CancellationToken cancellationToken, IsolationLevel isolationLevel = IsolationLevel.Serializable); Task CommitAsync(CancellationToken cancellationToken); Task RollbackAsync(CancellationToken cancellationToken); } ##### c. SqlUnitOfWork Implementation public class SqlUnitOfWork(MsSqlConfiguration configuration) : IUnitOfWork { public SqlConnection Connection { get; } = new(configuration.ConnectionString); public SqlTransaction? Transaction { get; private set; } public async Task BeginTransactionAsync(CancellationToken cancellationToken, IsolationLevel isolationLevel = IsolationLevel.Serializable) { if (Transaction == null) { if (Connection.State != ConnectionState.Open) { await Connection.OpenAsync(cancellationToken); } Transaction = Connection.BeginTransaction(isolationLevel); } } public async Task CommitAsync(CancellationToken cancellationToken) { if (Transaction != null) { await Transaction.CommitAsync(cancellationToken); } } public async Task RollbackAsync(CancellationToken cancellationToken) { if (Transaction != null) { await Transaction.RollbackAsync(cancellationToken); } } public async Task<SqlCommand> CreateSqlCommandAsync(string sql, SqlParameter[] parameters, CancellationToken cancellationToken) { if (Connection.State != ConnectionState.Open) { await Connection.OpenAsync(cancellationToken); } SqlCommand command = Connection.CreateCommand(); if (Transaction != null) { command.Transaction = Transaction; } command.CommandText = sql; if (parameters.Length > 0) { command.Parameters.AddRange(parameters); } return command; } } ##### d. Register Services in Dependency Injection services .AddScoped<SqlUnitOfWork, SqlUnitOfWork>() .TryAddScoped<IUnitOfWork>(provider => provider.GetRequiredService<SqlUnitOfWork>()); ## Conclusion By implementing the Outbox Pattern with Brighter and SQL Server, we’ve demonstrated how to achieve transactional consistency between database updates and message publishing. This approach ensures that: 1. Messages are only published if the transaction commits successfully * Using `DepositPostAsync`, messages like `OrderPlaced` and `OrderPaid` are stored in the `OutboxMessages` table within the same transaction as business data. If the handler fails (e.g., due to a simulated error), the transaction rolls back, and no orphaned messages are sent. * Brighter's `IMsSqlTransactionConnectionProvider` guarantees that database updates and message deposits share the same transaction. 2. Fault Tolerance via the Outbox Sweeper * The `UseOutboxSweeper` polls for undelivered messages and retries them until acknowledged by RabbitMQ. This decouples message publishing from the handler’s execution, ensuring reliability even if the broker is temporarily unavailable. 3. Decoupled Architecture * Applications focus on local transactions, while Brighter handles message delivery asynchronously. This avoids tight coupling to the messaging infrastructure and simplifies scalability. This implementation showcases how Brighter abstracts complexity, enabling developers to focus on business logic while ensuring reliability in distributed systems. For production use, pair this pattern with monitoring tools (e.g., Prometheus), dead-letter queues (DLQs) to handle poisoned messages and add index on the outbox table on `Dispatched` and `Timestamp` columns. ## Reference * Github with the full code
forem.com
June 2, 2025 at 8:30 AM
Simplifying System Design: Event-Carried State Transfer

In modern distributed systems, Event-Carried State Transfer (ECST) ensures that events contain all necessary data, eliminating the need for consumers to call back for additional information. This approach minimizes coupling, reduces failure…
Simplifying System Design: Event-Carried State Transfer
In modern distributed systems, Event-Carried State Transfer (ECST) ensures that events contain all necessary data, eliminating the need for consumers to call back for additional information. This approach minimizes coupling, reduces failure cascades, and enhances system resilience. You Should Know: Avoid Empty Events – Instead of OrderPlaced, send: { "event": "OrderPlaced", "order_id": "12345", "customer_id": "67890", "items": [{"id": "A1", "qty": 2}],
undercodetesting.com
June 3, 2025 at 1:31 PM
MOD-E's Code Corner 🧠: Event-Driven Arch (EDA) explained! No direct calls. Services emit 'events' (e.g., 'OrderPlaced'); others listen & react. Decouples systems for scalable, resilient, adaptable, modern apps. #CodeCorner
June 25, 2025 at 7:02 PM
Event sourcing and BDD share the same pattern: we can only capture what has happened, not what hasn't.

No more "Given I have not paid"—instead, "Given seven days have passed."

What events has your system captured?

lassala.net/2026/02/11/e...

#BDD #EventSourcing
Event Sourcing and the Past Tense of Given
I was thinking about event sourcing and event storming the other day, and something clicked. Events in these systems are always named in past tense: “OrderPlaced,” “PaymentReceived,” “InvoiceAged.”…
lassala.net
February 11, 2026 at 7:29 PM
Publish OrderShipped only when both OrderPlaced and OrderBilled have been successfully processed…how do we do that from stateless message handlers? Check out our #NServiceBus #Saga tutorial https://docs.particular.net/tutorials/nservicebus-sagas/1-saga-basics/
NServiceBus sagas: Saga basics
A step-by-step guide to building an NServiceBus saga to handle a common business case of taking action once multiple messages have been successfully received.
docs.particular.net
September 10, 2025 at 3:04 AM
What do you do when you can't take some action (like shipping an order) until 2 different messages (OrderPlaced and OrderBilled) have been processed? The answer is a saga, and we've got a guide to show you how to do it
NServiceBus sagas: Saga basics
A step-by-step guide to building an NServiceBus saga to handle a common business case of taking action once multiple messages have been successfully received.
docs.particular.net
June 4, 2025 at 3:04 AM
you ever ratio yourself
February 21, 2026 at 2:44 AM