Lesson 4.11.1 introduced the event-driven style as one of six, and lesson 4.11.2 recommended “asynchronous where you can” as the second step on the way from module to service. This lesson takes the style apart. In the Kladde, the word event now stands for at least three different things, and anyone who mixes them up builds either dependencies they wanted to avoid or copies nobody maintains. After that come the question of who steers a flow across several contexts, the three services Azure uses to distribute events, and serverless, which in Azure almost always shows up together with events.
At the end you sketch the flow from the tender through the quote to the deal twice, once without and once with a steering component, play through the failure cases and justify one of the two forms. Nothing changes in the Kladde itself; the decision gets built in module 4.12.
Five earlier lessons carry this one. Lesson 4.3.4 separated domain events from integration events. Module 4.4 built sales with event sourcing, events as storage. Lesson 4.6.2 got integration events out safely with the outbox, and lesson 4.6.3 received them idempotently with the inbox. And lesson 4.6.4 set choreography against orchestration and built the process manager for “quote accepted” as a grain. You know serverless from module 3.13 in K3.
Three things called “event”
An event is something that has happened, named in the past tense. Everyone agrees that far. What an event does is meant in three different ways in the Kladde:
| Event notification | Event-carried state transfer | Event sourcing | |
|---|---|---|---|
| What it says | “Tender ted-4711 is marked as relevant.” | “… marked, title …, submission deadline March 28” | “Deal qualified”, as one entry in the deal's stream |
| What for | get others' attention | give others what they need | store one's own state |
| Who reads | other contexts; they ask back if they need more | other contexts; they keep a copy | the aggregate itself and its projections |
| Coupling at runtime | high: whoever asks back needs the source | low: the copy is enough | none to the outside |
| Price | load and outage at the source | larger messages, copies that are briefly stale, versioning | everything from module 4.4 |
| In the Kladde | QuoteAcceptedEvent (identifiers only, lesson 4.6.2) |
none yet | sales (module 4.4) |
The first two are kinds of communication between contexts. Microsoft describes them for the event-driven style as two ways to fill a message: send all the attributes a consumer needs, or send only the keys. All attributes spare the consumer the call back, but they cost bandwidth, make contracts and versions harder and create several places where the same value lives. Keys only keep the data in one place, but they cost calls back to the source.
Event sourcing is something else: a way to store data. The events in a deal's stream are the inside of sales. They
never leave it directly, only translated, the way the relay in lesson 4.6.2 turns the stored DealWon into an integration event. Anyone who
confuses event sourcing with event-driven architecture makes their storage format a contract with others, and Microsoft lists exactly that
among the typical mistakes of the microservices style: publishing database contents as events.
A small program shows what the difference between the first two means in operation. Sales remembers every tender that is marked as relevant,
with its submission deadline. Once it gets a notification and asks back, once an event with state. Create it in your exercise folder as
event-kinds.cs:
// event-kinds.cs
// Sales remembers every tender that is marked as relevant, with its submission deadline; a deal can come out of it.
// Once only a notification arrives (sales asks back), once an event with state (sales has a copy).
#:property PublishAot=false
using System.Globalization;
var tenders = new Tenders();
tenders.Add("ted-4711", "Network, Erlengrund Elementary School", new DateOnly(2025, 3, 28));
// The tenders context marks ted-4711 as relevant and publishes the event, here in both versions.
var notification = new TenderMarkedRelevant("ted-4711");
var withState = new TenderMarkedRelevantV2("ted-4711", "Network, Erlengrund Elementary School", new DateOnly(2025, 3, 28));
var notified = new SalesWithNotifications(tenders);
var carried = new SalesWithStateTransfer();
Console.WriteLine($"A1 Notification, source running: {notified.On(notification)}");
tenders.Running = false;
Console.WriteLine($"A2 Notification, source stopped: {notified.On(notification)}");
Console.WriteLine($"B1 Event with state, source stopped: {carried.On(withState)}");
// The deadline is moved. The event about it is still in transit: for this time the copy is stale.
tenders.Running = true;
tenders.MoveDeadline("ted-4711", new DateOnly(2025, 4, 4));
Console.WriteLine($"B2 Deadline moved, event in transit: copy {Day(carried.DeadlineOf("ted-4711"))}, source {Day(tenders.AskDeadline("ted-4711"))}");
carried.On(new TenderDeadlineMoved("ted-4711", new DateOnly(2025, 4, 4)));
Console.WriteLine($"B3 Event arrived: copy {Day(carried.DeadlineOf("ted-4711"))}, source {Day(tenders.AskDeadline("ted-4711"))}");
static string Day(DateOnly? day) => day?.ToString("MM/dd", CultureInfo.InvariantCulture) ?? "none";
// The tenders' contracts: two versions of the same event and a second event for the change.
sealed record TenderMarkedRelevant(string TenderId);
sealed record TenderMarkedRelevantV2(string TenderId, string Title, DateOnly SubmissionDeadline);
sealed record TenderDeadlineMoved(string TenderId, DateOnly SubmissionDeadline);
// The tenders context, with a switch for “is not running right now”.
sealed class Tenders
{
private readonly Dictionary<string, (string Title, DateOnly Deadline)> tenders = [];
public bool Running { get; set; } = true;
public void Add(string id, string title, DateOnly deadline) => tenders[id] = (title, deadline);
public void MoveDeadline(string id, DateOnly deadline) => tenders[id] = tenders[id] with { Deadline = deadline };
// The query other contexts make. If the context is not running, there is no answer.
public DateOnly? AskDeadline(string id) => Running ? tenders[id].Deadline : null;
}
// Sales with notifications: a small event, but dependent on the tenders at runtime.
sealed class SalesWithNotifications(Tenders tenders)
{
public string On(TenderMarkedRelevant marked) =>
tenders.AskDeadline(marked.TenderId) is { } deadline
? $"noted, deadline {deadline.ToString("MM/dd", CultureInfo.InvariantCulture)}"
: "not noted, deadline not available";
}
// Sales with events with state: independent at runtime, but with a copy that can briefly go stale.
sealed class SalesWithStateTransfer
{
private readonly Dictionary<string, DateOnly> deadlines = [];
public string On(TenderMarkedRelevantV2 marked)
{
deadlines[marked.TenderId] = marked.SubmissionDeadline;
return $"noted, deadline {marked.SubmissionDeadline.ToString("MM/dd", CultureInfo.InvariantCulture)}";
}
public void On(TenderDeadlineMoved moved) => deadlines[moved.TenderId] = moved.SubmissionDeadline;
public DateOnly? DeadlineOf(string tenderId) => deadlines.TryGetValue(tenderId, out var deadline) ? deadline : null;
}
dotnet run event-kinds.cs
A1 Notification, source running: noted, deadline 03/28
A2 Notification, source stopped: not noted, deadline not available
B1 Event with state, source stopped: noted, deadline 03/28
B2 Deadline moved, event in transit: copy 03/28, source 04/04
B3 Event arrived: copy 04/04, source 04/04
- A2 shows the price of the notification. The event has arrived, but without the source it is of no use. Whoever works like this has only moved the coupling they wanted to get rid of with the event into the consumer. A receiver that fails here has to process the message again later, and the inbox from lesson 4.6.3 has to allow it to do so.
- B1 shows the gain of the event with state: Sales works even though the tenders are down.
- B2 shows its price: for the time the event about the change is in transit, sales knows the old deadline. Whoever transfers state has to transfer every change too, and every consumer sees a stale state for a while. Lesson 4.11.4 turns that into a consistency question with a name.
One rule from the house rules applies to the Kladde as well: personal data never goes into events. An event with state may carry the submission deadline and the title of a public tender, but not the name and address of a contact person. There it stays with the identifier, and whoever needs the name asks through the contacts' contract.