Introduction

Modern cloud-native applications demand scalability, resilience, and performance. Distributed architectures enable systems to meet those demands. But with great distribution comes great complexity. In this post, we’ll explore synchronous and asynchronous patterns in distributed systems and finish with caching patterns that help improve efficiency.


🧭 1. Synchronous Communication Patterns

Synchronous communication happens when a service waits for a response from another before continuing execution. This is often done over HTTP.

📦 Pattern: API Composition

Use when multiple services must be queried to fulfill a request.

Example: Aggregating Responses from Microservices

public class OrderAggregatorService(HttpClient httpClient)
{
    public async Task<OrderDetailsDto> GetOrderDetailsAsync(Guid orderId)
    {
        var orderTask = httpClient.GetFromJsonAsync<OrderDto>($"http://orders/api/orders/{orderId}");
        var customerTask = httpClient.GetFromJsonAsync<CustomerDto>($"http://customers/api/customers/{orderId}");

        await Task.WhenAll(orderTask, customerTask);

        return new OrderDetailsDto
        {
            Order = await orderTask,
            Customer = await customerTask
        };
    }
}

Pros:

  • Simpler to implement.
  • Immediate feedback to the client.

Cons:

  • Tight coupling.
  • Latency grows with each additional service call.

🕊️ 2. Asynchronous Communication Patterns

These patterns rely on message queues, event brokers, and event-driven design. They enable decoupled, resilient services.

🧩 Pattern: Event-Driven Architecture

Services emit and subscribe to domain events via a message broker like RabbitMQ or Kafka.

Example: Order Service Publishing an Event

public class OrderService(IMessageBus messageBus)
{
    public async Task PlaceOrderAsync(Order order)
    {
        // Save order to DB (omitted)
        
        var orderPlaced = new OrderPlacedEvent(order.Id, order.CustomerId);
        await messageBus.PublishAsync(orderPlaced);
    }
}

Consumer: Inventory Service Listens

public class OrderPlacedHandler : IMessageHandler<OrderPlacedEvent>
{
    public async Task HandleAsync(OrderPlacedEvent evt)
    {
        Console.WriteLine($"Reserving stock for Order {evt.OrderId}");
        // Call internal logic to reserve inventory
    }
}

Pros:

  • Loose coupling.
  • High scalability and resilience.

Cons:

  • Harder to debug.
  • Eventual consistency requires careful handling.

🧠 3. Caching Patterns

To reduce load on services and databases, caching plays a crucial role. Caching is typically combined with both sync and async patterns.


🎯 Pattern: Cache Aside (Lazy Loading)

Data is fetched from cache if available, otherwise loaded from source and cached.

Example with Redis and StackExchange.Redis

public class ProductService(IConnectionMultiplexer redis, HttpClient httpClient)
{
    private readonly IDatabase _cache = redis.GetDatabase();

    public async Task<ProductDto> GetProductAsync(Guid productId)
    {
        var key = $"product:{productId}";
        var cached = await _cache.StringGetAsync(key);
        if (cached.HasValue)
            return JsonSerializer.Deserialize<ProductDto>(cached!);

        var product = await httpClient.GetFromJsonAsync<ProductDto>($"http://products/api/products/{productId}");
        if (product is not null)
        {
            await _cache.StringSetAsync(key, JsonSerializer.Serialize(product), TimeSpan.FromMinutes(30));
        }

        return product!;
    }
}

🧯 Pattern: Write-Through Cache

Writes go through the cache, and the cache updates the database.

Example with MemoryCache

public class CustomerService(IMemoryCache memoryCache, ICustomerRepository db)
{
    public async Task SaveCustomerAsync(Customer customer)
    {
        // Save to DB first
        await db.SaveAsync(customer);

        // Update cache immediately
        memoryCache.Set($"customer:{customer.Id}", customer, TimeSpan.FromHours(1));
    }
}

⚡ Pattern: Refresh Ahead

Preemptively refresh cache before expiry to avoid stale data or cache misses.

Example with BackgroundService

public class ProductCacheRefresher : BackgroundService
{
    private readonly IProductRepository _repo;
    private readonly IMemoryCache _cache;

    public ProductCacheRefresher(IProductRepository repo, IMemoryCache cache)
    {
        _repo = repo;
        _cache = cache;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var allProducts = await _repo.GetAllAsync();
            foreach (var product in allProducts)
            {
                _cache.Set($"product:{product.Id}", product, TimeSpan.FromMinutes(30));
            }

            await Task.Delay(TimeSpan.FromMinutes(25), stoppingToken);
        }
    }
}

🎁 Summary

TopicPatternBenefitTrade-Off
Synchronous CommunicationAPI CompositionSimpler orchestrationTight coupling, latency
Asynchronous CommunicationEvent-DrivenLoose coupling, scalabilityEventual consistency, debugging
CachingCache-AsideLow latency readsSlight cache miss penalty
Write-ThroughAlways-fresh readsWrite overhead
Refresh-AheadPrevents cache stampedesBackground complexity

✅ Final Thoughts

Choosing the right architecture pattern depends on your latency, resilience, and consistency needs. In many systems, a hybrid approach—mixing synchronous and asynchronous patterns with smart caching—is the best path forward.

.NET 9 brings performance improvements and minimal APIs that make implementing these patterns more productive than ever. When combined with distributed tracing and metrics, you’re well on your way to building robust cloud-native applications.


Views: 9

🚀 Distributed Architecture Patterns & Caching Strategies in .NET 9

Johannes Rest


.NET Architekt und Entwickler


Beitragsnavigation


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert