Enterprise Integration Patterns for Modern .NET Applications

Every organization above a certain size runs on integrations. CRM talks to ERP. ERP talks to the data warehouse. The data warehouse feeds the reporting portal. A payroll system pulls from HR. Identity flows through everything. When these connections work, nobody notices. When they break, everyone notices immediately.
The challenge is not building integrations -- any competent team can connect two systems. The challenge is building integrations that remain reliable, observable, and adaptable as the systems on both ends inevitably change. After two decades of enterprise integration work, I have found that the difference between fragile and resilient comes down to a small number of architectural decisions made early.
The integration anti-patterns
Before discussing what works, it helps to name what does not:
- Point-to-point spaghetti -- every system connected directly to every other system. Adding system N+1 requires N new connections. Removing a system is terrifying.
- Shared database integration -- two applications reading and writing to the same tables. Schema changes become multi-team negotiations.
- Synchronous chains -- System A calls System B which calls System C. If C is slow, everything is slow. If C is down, everything is down.
- Fire-and-forget -- messages sent with no confirmation, no retry, and no visibility into failures. Data silently diverges between systems.
If any of these describe your current architecture, you are not alone. They are the natural result of building integrations under deadline pressure without a deliberate strategy.
The patterns that last
Modern .NET gives us excellent building blocks for durable integration. Here are the patterns I reach for most often:
Event-driven messaging
Instead of systems calling each other directly, they publish events to a message broker. Interested systems subscribe. This decouples producers from consumers, allows independent scaling, and means a slow consumer does not block the producer.
// Publishing a domain event
public class OrderService
{
private readonly IMessageBus _bus;
public async Task PlaceOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _bus.PublishAsync(new OrderPlacedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Total = order.Total,
OccurredAt = DateTimeOffset.UtcNow
});
}
}Azure Service Bus and RabbitMQ are the two brokers I recommend most for .NET shops. Service Bus for Azure-native workloads with strong ordering and transaction guarantees. RabbitMQ for on-premises or hybrid scenarios where you need full control.
The outbox pattern
The classic problem with event-driven systems: what happens if the database save succeeds but the message publish fails? Or vice versa? You get data inconsistency.
The outbox pattern solves this by writing the event to an outbox table in the same database transaction as the business data, then publishing it asynchronously:
// Transactional outbox
await using var transaction = await _db.BeginTransactionAsync();
_db.Orders.Add(order);
_db.OutboxMessages.Add(new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(OrderPlacedEvent),
Payload = JsonSerializer.Serialize(orderPlacedEvent),
CreatedAt = DateTimeOffset.UtcNow,
ProcessedAt = null
});
await _db.SaveChangesAsync();
await transaction.CommitAsync();A background worker polls the outbox table and publishes unprocessed messages. If publishing fails, it retries. If the original transaction fails, neither the order nor the message is committed. Consistency is guaranteed.
API gateway and contract-first design
For synchronous integrations that cannot be made asynchronous -- real-time lookups, authentication flows, user-facing queries -- use a well-defined API gateway with versioned contracts.
The key discipline is contract-first design: define the API schema before writing any code. Use OpenAPI specifications as the single source of truth. Generate client SDKs from the spec. This prevents the drift that occurs when APIs evolve informally.
# openapi.yaml
paths:
/api/v1/customers/{id}:
get:
operationId: getCustomer
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Customer found
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
'404':
description: Customer not foundIdempotent consumers
In distributed systems, messages will occasionally be delivered more than once. Network glitches, consumer restarts, and broker redeliveries all cause duplicates. Every consumer must handle this gracefully.
The simplest approach is an idempotency key -- a unique identifier per message that the consumer checks before processing:
public async Task HandleAsync(OrderPlacedEvent evt)
{
if (await _processed.ContainsAsync(evt.EventId))
return; // Already handled
await _invoiceService.CreateInvoiceAsync(evt);
await _processed.MarkAsync(evt.EventId);
}This is not optional. It is a requirement for any integration that cannot tolerate duplicate processing.
Identity as the integration backbone
Every integration eventually needs to answer the question: who is making this request, and are they allowed? In organizations running Microsoft 365 or Azure, Entra ID (formerly Azure AD) is the natural answer.
A unified identity layer means:
- Single sign-on across all integrated systems.
- OAuth 2.0 / OpenID Connect for API authentication, replacing shared secrets and API keys.
- Conditional access policies that apply consistently whether the user is accessing the CRM, the portal, or the data warehouse.
- Audit trails that trace actions across systems back to a single identity.
Investing in identity infrastructure early pays dividends across every integration you build afterward.
Observability across boundaries
The hardest part of debugging distributed integrations is tracing a request across system boundaries. Structured logging with correlation IDs makes this tractable:
- Every inbound request generates or propagates a correlation ID.
- Every log entry, every message published, and every downstream call includes that ID.
- Centralized logging (Application Insights, Seq, or the ELK stack) lets you reconstruct the full journey of any request.
Without this, troubleshooting a failed integration means manually correlating timestamps across multiple systems -- a process that turns a 10-minute investigation into a multi-hour archaeology expedition.
Choosing your integration strategy
There is no single correct integration pattern. The right choice depends on the specific requirements:
- Real-time, user-facing -- synchronous API with gateway.
- Business process, eventual consistency acceptable -- event-driven messaging with outbox.
- Batch data synchronization -- scheduled ETL with idempotent loads.
- Legacy system with no API -- database integration via change data capture as a last resort, with a clear plan to migrate away.
The mistake is applying the same pattern everywhere. The skill is matching the pattern to the problem.
Building for change
The systems on both ends of an integration will change. Vendors ship updates. Internal teams refactor. Schemas evolve. Regulations shift. The integrations that survive are the ones built with change in mind:
- Version your APIs and message contracts.
- Use schema evolution strategies (additive changes only, no breaking removals).
- Wrap third-party system calls behind anti-corruption layers so vendor changes do not ripple through your codebase.
- Monitor integration health continuously, not just when something breaks.
Enterprise integration is not glamorous work, but it is the connective tissue that determines whether an organization's technology landscape feels coherent or chaotic. Getting the patterns right early is one of the highest-leverage investments a technology leader can make.
Founder & Principal Consultant at VerionSys. 24+ years delivering enterprise systems across AI, cloud, and integration in Brazil, Canada, and the USA.
Connect on LinkedIn