Oct 08, 2025

Enterprise Architecture Patterns for .NET Applications

Learn proven enterprise architecture patterns that power scalable, maintainable, and robust .NET applications for large-scale businesses.

RD
Robert Davis
• 5 min read

Introduction to Enterprise Architecture

Enterprise architecture defines the blueprint for how an organization's IT infrastructure, processes, and applications work together. For .NET applications serving thousands of users, choosing the right architectural patterns is crucial for success.

Key Architecture Goals
  • Scalability: Handle growing user loads and data volumes
  • Maintainability: Easy to modify and extend over time
  • Reliability: Consistent performance and fault tolerance
  • Security: Protect sensitive business data
  • Performance: Meet business SLA requirements

Clean Architecture Pattern

Clean Architecture, popularized by Robert C. Martin, provides a solid foundation for enterprise applications by separating concerns into distinct layers:

Architecture Layers
  • Domain Layer: Business entities and rules
  • Application Layer: Use cases and orchestration
  • Infrastructure Layer: External concerns
  • Presentation Layer: UI and API controllers
Dependency Direction

Dependencies point inward toward the domain layer, ensuring business logic is isolated from external concerns like databases and frameworks.

Domain Entity Example

// Domain Layer - Core business entity
public class Order
{
    public int Id { get; private set; }
    public string OrderNumber { get; private set; }
    public DateTime OrderDate { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    
    private readonly List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

    public Order(string orderNumber, DateTime orderDate)
    {
        OrderNumber = orderNumber;
        OrderDate = orderDate;
        Status = OrderStatus.Pending;
    }

    public void AddItem(Product product, int quantity, decimal unitPrice)
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Cannot modify a processed order");

        var item = new OrderItem(product.Id, quantity, unitPrice);
        _items.Add(item);
        RecalculateTotal();
    }

    public void ProcessOrder()
    {
        if (!_items.Any())
            throw new InvalidOperationException("Cannot process an empty order");

        Status = OrderStatus.Processing;
        // Domain events could be raised here
    }

    private void RecalculateTotal()
    {
        TotalAmount = _items.Sum(item => item.Quantity * item.UnitPrice);
    }
}

CQRS (Command Query Responsibility Segregation)

CQRS separates read and write operations, optimizing each for their specific use case. This pattern is especially valuable for enterprise applications with complex reporting needs.

Commands (Write)
  • Change application state
  • Business rule validation
  • Event generation
  • Transactional consistency
Queries (Read)
  • Read-only operations
  • Optimized for specific views
  • Caching strategies
  • Denormalized data

CQRS Implementation

// Command
public class CreateOrderCommand : IRequest<int>
{
    public string CustomerEmail { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(GenerateOrderNumber(), DateTime.UtcNow);
        
        foreach (var item in request.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            order.AddItem(product, item.Quantity, item.UnitPrice);
        }

        await _repository.AddAsync(order);
        await _unitOfWork.SaveChangesAsync();
        
        return order.Id;
    }
}

// Query
public class GetOrderDetailsQuery : IRequest<OrderDetailsDto>
{
    public int OrderId { get; set; }
}

public class GetOrderDetailsQueryHandler : IRequestHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
    private readonly IReadOnlyDbContext _context;

    public async Task<OrderDetailsDto> Handle(GetOrderDetailsQuery request, CancellationToken cancellationToken)
    {
        return await _context.Orders
            .Where(o => o.Id == request.OrderId)
            .Select(o => new OrderDetailsDto
            {
                Id = o.Id,
                OrderNumber = o.OrderNumber,
                TotalAmount = o.TotalAmount,
                Items = o.Items.Select(i => new OrderItemDto
                {
                    ProductName = i.Product.Name,
                    Quantity = i.Quantity,
                    UnitPrice = i.UnitPrice
                }).ToList()
            })
            .FirstOrDefaultAsync(cancellationToken);
    }
}

Service Communication

// HTTP Client for service-to-service communication
public class OrderService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _configuration;

    public OrderService(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _configuration = configuration;
    }

    public async Task<bool> ProcessPaymentAsync(int orderId, decimal amount)
    {
        var paymentRequest = new
        {
            OrderId = orderId,
            Amount = amount,
            Currency = "USD"
        };

        var response = await _httpClient.PostAsJsonAsync(
            $"{_configuration["PaymentService:BaseUrl"]}/api/payments", 
            paymentRequest);

        return response.IsSuccessStatusCode;
    }
}

// Startup configuration
builder.Services.AddHttpClient<OrderService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["PaymentService:BaseUrl"]);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

Event-Driven Architecture

Event-driven patterns enable loose coupling between services and support scalable, resilient architectures through asynchronous communication.

Domain Events

Business events within bounded contexts

Integration Events

Cross-service communication events

Event Sourcing

Store events as the source of truth

Event Publishing

// Domain Event
public class OrderCreatedEvent : INotification
{
    public int OrderId { get; }
    public string CustomerEmail { get; }
    public decimal TotalAmount { get; }
    public DateTime CreatedAt { get; }

    public OrderCreatedEvent(int orderId, string customerEmail, decimal totalAmount)
    {
        OrderId = orderId;
        CustomerEmail = customerEmail;
        TotalAmount = totalAmount;
        CreatedAt = DateTime.UtcNow;
    }
}

// Event Handler
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;

    public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        // Send confirmation email
        await _emailService.SendOrderConfirmationAsync(
            notification.CustomerEmail, 
            notification.OrderId);

        // Update inventory
        await _inventoryService.ReserveItemsAsync(notification.OrderId);
    }
}

Choosing the Right Pattern

The choice of architecture pattern depends on your specific requirements:

Pattern Best For Complexity Team Size
Clean Architecture Most enterprise applications Medium 5-15 developers
CQRS Complex read/write scenarios Medium-High 8+ developers
Microservices Large-scale, multi-team projects High 20+ developers
Event-Driven Asynchronous, decoupled systems High 10+ developers

Need Architecture Guidance?

Our enterprise architecture experts can help you choose and implement the right patterns for your .NET applications.