Event-driven architecture has become a common pattern for modern .NET applications.
Instead of sending every operation through synchronous APIs, services publish events such as:
OrderCreated
PaymentCompleted
CustomerUpdated
InventoryChanged
DocumentProcessed
Other services consume those events asynchronously.
This works well when event producers and consumers evolve independently. But introducing AI agents into the architecture creates a new challenge.
An AI system may consume events to:
Trigger workflows
Build customer context
Detect operational anomalies
Generate summaries
Update recommendations
Start agent tasks
Feed retrieval systems
Create business alerts
At that point, an event is no longer just an integration message.
It can become input to an AI decision-making system.
That makes the quality, stability, semantics, and governance of the event contract much more important.
The solution is not to create a special "AI event format" for everything.
Instead, design event contracts so that they are explicit, versioned, backward-compatible, semantically clear, and safe for machine consumption.
What Is a Data Contract?
A data contract defines what information a producer guarantees to provide and what consumers can expect.
For an event such as:
{
"eventType": "OrderCreated",
"orderId": "ORD-1001",
"customerId": "CUS-204",
"totalAmount": 249.99
}
the contract should answer questions such as:
What does
orderIdrepresent?Is it globally unique?
What format does
totalAmountuse?Which currency applies?
Is
customerIdoptional?Can fields be removed?
How are new fields introduced?
What does an event timestamp mean?
Without these rules, consumers end up interpreting the payload differently.
For traditional applications, this creates integration bugs.
For AI systems, it can create incorrect decisions.
Why AI Changes the Contract Problem
Consider this event:
{
"customer": "Acme",
"value": 1000,
"status": "active"
}
A human developer might understand what these fields mean from application context.
An AI system cannot safely assume that:
value = revenue
or:
status = customer account status
The field could represent something completely different.
A better contract is explicit:
{
"eventType": "CustomerRevenueUpdated",
"schemaVersion": "1.0",
"customerId": "CUS-204",
"revenue": {
"amount": 1000.00,
"currency": "USD"
},
"effectiveAt": "2026-08-12T09:30:00Z"
}
Now the semantics are much clearer.
This matters when an agent uses event data to make decisions.
Event-Driven AI Architecture
A typical architecture can look like this:
Business Services
|
v
Event Broker
|
+------------+------------+
| | |
v v v
.NET Worker AI Pipeline Analytics
| |
| v
| AI Agent
| |
| +----+----+
| | |
v v v
Database Tools Knowledge
The event broker could be backed by services such as Kafka, Amazon EventBridge, Azure Event Hubs, or another messaging platform.
The important architectural principle is:
Producer
|
v
Stable Contract
|
v
Event Infrastructure
|
+--> Traditional Consumer
|
+--> AI Consumer
|
+--> Analytics Consumer
The producer should not need to know which consumers are AI-powered.
Separate Business Events From AI Prompts
One common mistake is designing an event specifically for an LLM:
{
"prompt": "Analyze this order and decide whether it is suspicious..."
}
This creates unnecessary coupling.
The business event should describe the business fact:
{
"eventType": "OrderCreated",
"orderId": "ORD-1001",
"customerId": "CUS-204",
"totalAmount": 249.99
}
An AI consumer can then construct the appropriate reasoning context.
Business Event
|
v
AI Consumer
|
+--> Enrich Context
|
+--> Apply Policy
|
+--> Build Prompt
|
v
AI Model
This separation allows the same event to support multiple consumers.
Design Events Around Business Facts
A useful event should describe something that happened.
Prefer:
OrderCreated
PaymentCaptured
CustomerAddressChanged
InventoryReserved
DocumentUploaded
over:
ProcessOrder
AnalyzeCustomer
RunAI
GenerateRecommendation
The first group describes facts.
The second group describes actions or implementation details.
This distinction makes event contracts more reusable.
Example .NET Event Contract
A strongly typed C# contract might look like:
public sealed record OrderCreatedEvent
{
public required string EventId { get; init; }
public required string EventType { get; init; }
public required int SchemaVersion { get; init; }
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public required decimal TotalAmount { get; init; }
public required string Currency { get; init; }
public required DateTimeOffset OccurredAt { get; init; }
}
The contract explicitly identifies:
Event identity
Event type
Schema version
Business identifiers
Monetary value
Currency
Event timestamp
This is preferable to passing an anonymous dictionary through the system.
Add a Contract Envelope
A useful event structure separates metadata from business data.
For example:
{
"id": "evt-12345",
"type": "OrderCreated",
"version": 1,
"source": "orders-service",
"occurredAt": "2026-08-12T09:30:00Z",
"correlationId": "corr-98765",
"data": {
"orderId": "ORD-1001",
"customerId": "CUS-204",
"totalAmount": 249.99,
"currency": "USD"
}
}
This gives infrastructure and consumers consistent metadata.
The data section contains business information.
The envelope contains information about the event itself.
Event ID and Idempotency
AI consumers can process events asynchronously and may retry failed messages.
Therefore, every event should have a stable identifier.
For example:
eventId = evt-12345
The consumer can use it to detect duplicates.
A simplified .NET consumer might use:
public async Task HandleAsync(
OrderCreatedEvent message,
CancellationToken cancellationToken)
{
if (await _store.ExistsAsync(
message.EventId,
cancellationToken))
{
return;
}
await ProcessAsync(message, cancellationToken);
await _store.MarkProcessedAsync(
message.EventId,
cancellationToken);
}
This is particularly important for AI workflows.
If the same event triggers an expensive model invocation twice, the application may incur unnecessary processing or produce duplicate actions.
At-Least-Once Delivery Changes the Design
Many event systems provide at-least-once delivery semantics.
That means consumers should be prepared to see the same event more than once.
The correct design is generally:
Receive Event
|
v
Check Idempotency
|
+---- Already processed ---> Stop
|
v
Process
|
v
Persist Result
|
v
Mark Event Complete
Do not assume:
One event = exactly one processing attempt
unless your infrastructure and application explicitly guarantee that behavior.
Schema Evolution
Data contracts will change.
Suppose version 1 contains:
{
"orderId": "ORD-1",
"totalAmount": 100
}
Later, the producer adds:
{
"orderId": "ORD-1",
"totalAmount": 100,
"currency": "USD"
}
Adding an optional field is generally easier to handle than removing or changing the meaning of an existing field.
A consumer should tolerate fields it does not understand.
For example:
public sealed record OrderCreatedEvent
{
public required string OrderId { get; init; }
public decimal TotalAmount { get; init; }
public string? Currency { get; init; }
}
Consumers can continue operating when Currency is absent if the contract defines an appropriate default or fallback behavior.
Never Change Field Meaning Silently
This is dangerous:
Version 1:
status = payment status
Version 2:
status = fulfillment status
The JSON shape has not changed.
But the semantic contract has.
An AI consumer could continue interpreting status according to the old meaning.
This is one reason semantic changes should be treated as contract changes even when the JSON schema remains valid.
Version Contracts Explicitly
There are several strategies for versioning.
Version in the Event Type
OrderCreated.v1
OrderCreated.v2
Version in Metadata
{
"type": "OrderCreated",
"version": 2
}
Schema Registry
A schema registry can maintain versions independently of the event name.
The choice depends on the messaging infrastructure.
The important rule is consistency.
Consumers should know exactly which contract they are processing.
Avoid Breaking AI Consumers
Suppose an AI pipeline expects:
customerId
orderId
totalAmount
currency
The producer removes:
currency
The traditional application may fail quickly.
An AI system might still generate a plausible response using incomplete context.
That can be more dangerous.
For example:
Order value: 1,000
Currency: unknown
The model could incorrectly infer the currency from unrelated context.
Therefore, AI consumers should validate contract completeness before reasoning.
if (string.IsNullOrWhiteSpace(message.Currency))
{
throw new InvalidOperationException(
"Currency is required for AI analysis.");
}
A failed workflow is preferable to a confident decision based on incomplete data.
Make Units Explicit
Avoid ambiguous fields:
{
"temperature": 30
}
Is that Celsius or Fahrenheit?
Prefer:
{
"temperature": {
"value": 30,
"unit": "C"
}
}
The same principle applies to:
Currency
Weight
Distance
Duration
Percentage
Tax
Exchange rates
AI systems are especially sensitive to ambiguous numerical context.
Dates and Time Zones
Use explicit timestamps.
Prefer:
{
"occurredAt": "2026-08-12T09:30:00Z"
}
over:
{
"date": "08/12/2026"
}
Also distinguish between:
occurredAt
createdAt
processedAt
effectiveAt
expiresAt
These timestamps represent different concepts.
An AI agent analyzing trends can easily produce an incorrect conclusion if the event timestamp semantics are unclear.
Do Not Put Large Documents Directly in Events
An event should generally identify a document rather than contain an unnecessarily large document payload.
Instead of:
{
"document": "very-large-base64-content..."
}
prefer:
{
"documentId": "DOC-1001",
"storageUri": "..."
}
Then the consumer can retrieve the document through an authorized storage interface.
For AI systems, this also enables:
Event
|
v
Document Reference
|
v
Authorized Retrieval
|
v
Chunking
|
v
Embedding / Analysis
This keeps the event contract smaller and makes access control explicit.
Separate Event Data From Retrieved Context
An AI workflow often needs more information than the event contains.
For example:
OrderCreated
|
v
Customer ID
|
+--> CRM
|
+--> Order History
|
+--> Inventory
|
+--> Payment History
The event should not attempt to contain the entire enterprise context.
Instead:
Event = Trigger + Stable Facts
and:
Retrieval = Additional Context
This makes the architecture more flexible.
Use Correlation and Causation IDs
When an event triggers an AI workflow, tracing becomes important.
For example:
{
"id": "evt-200",
"correlationId": "order-1001",
"causationId": "evt-199",
"type": "PaymentCompleted"
}

Join the conversation! Your thoughts help the community grow.