# Perché probabilmente non dovresti usare i microservizi (ancora)

<!--category-- Architecture, Microservices, Opinion -->
<datetime class="hidden">2025-12-29T20:00</datetime>

I microservizi sono diventati l'architettura predefinita del "sistema serio."

Se si vuole sembrare maturo, si parla di mesh di servizio, bus eventi, tracciamento distribuito, e "dispiegabilità indipendente." Se si vuole guardare enterprise-ready, si disegna caselle fino a quando il diagramma sembra una ciotola di spaghetti qualcuno ha lanciato a una lavagna bianca.

Ma qui c'è il correttivo che la maggior parte dei junior non vengono dati:

**I microservizi non sono un aggiornamento dell'architettura delle applicazioni, ma una strategia organizzativa di scalatura.**

Se non hai già il dolore organizzativo che risolvono, adottarli in anticipo non ti rende a prova di futuro.

Questo non è un argomento teorico, è un trade-off ingegneristico, e come tutti i compromessi, ha senso solo quando capisci la tassa che stai pagando e quello che ricevi in cambio.

Sono stato colpevole come chiunque di "fare microservizi" senza internalizzare completamente l'overhead organizzativo che comportano. E 'facile farsi coinvolgere nel vocabolario e dimenticare che la vera sfida è la gestione della complessità tra team e sistemi.

Alla fine si tratta del modello più antico in ingegneria software: **KISS** - Sii semplice, stupido.

[TOC]

## Il mito dei microservizi

I microservizi sono venduti come architettura predefinita perché la narrazione è seducente:

* I piccoli servizi sono più "puliti"
* I sistemi distribuiti sono "moderni"
* L'autonomia è "gratuita"
* Puoi "scalare più tardi" perché hai iniziato "giusto"

Quest'inquadratura e' all'indietro.

I microservizi non riguardano principalmente la struttura del codice. **chi può cambiare cosa, senza parlare con chi, e quanto spesso**.

Se non hai:

* Squadre multiple che spediscono in modo indipendente
* Priorità in conflitto
* Ostacoli al coordinamento
* Contesa di impiego
* Limiti di proprietà reale

Non stai risolvendo un problema organizzativo, ne stai comprando uno.

L'architettura, in ultima analisi, consiste nell'abilitare le persone, non spostare scatole.

## Il diagramma che dovrebbe essere un'etichetta di avvertimento

Hai visto questo diagramma.

![Un diagramma di architettura dei microservizi di avvertimento](bad_microservices.png?format=webp&height=600)

Questa non è una "architettura di esempio da copiare." È un'etichetta di avviso.

I corsi insegnano spesso i microservizi come diagrammi perché i diagrammi sono più facili dell'insegnamento della responsabilità operativa.

L'importante reframing è questo: quel diagramma non è uno stato bersaglio. **superficie di costo**.

Ogni scatola è un impegno:

* Un runtime da operare
* Un deployment da gestire
* Un contratto per la versione
* Una modalità di guasto che ora possiedi
* Una storia di guardia che non puoi ignorare.

Le frecce sembrano impressionanti, nascondono anche la realtà.

Perché ogni freccia è:

* Latenza
* Fallimento parziale
* Ritiri
* Timeout
* Propagazione del contesto di traccia
* "Lavorare in scena" mente

Se un'architettura richiede questo il primo giorno, non si sta costruendo un prodotto. Si sta costruendo una piattaforma.

## Il modello di costo nascosto dei microservizi

Come tutte le decisioni architettoniche, i microservizi sono dotati di **Tassa di complessità** (vedere [Giocare con il codice: la tassa di complessità](/blog/playing-with-code-efficient-agile)La differenza è che questa tassa composti attraverso ogni confine di servizio, ogni spiegamento, e ogni modalità di guasto.

I costi sono raramente espliciti, di solito sono travestiti da "migliori pratiche."

### Sovraffollamento operativo

Ogni servizio vuole:

* La propria pipeline di costruzione
* La propria configurazione di distribuzione
* Il proprio monitoraggio e l'allerta
* I suoi propri registri, dashboard, runbook
* La sua posizione di sicurezza (segreti, auth, ingressi)

Se una squadra non può farlo comodamente, non ha microservizi. **un monolito distribuito con gradini supplementari**.

### Latenza e moltiplicazione dei guasti

In un monolito, una chiamata è una chiamata di funzione.

Nei microservizi, una "chiamata" è una tregua negoziata tra:

* Reti
* Bilanciatori di carico
* TLS
* Middleware auth
* Serilizzazione
* Timeout
* Ritiri
* Contropressione

Le modalità di guasto si moltiplicano:

* Uno a valle è lento → upstream threads pile up
* Ricerchi amplificare il carico → DDoS te stesso educatamente
* Un unico flap di dipendenza → tre servizi degradano "casualmente"

Non debug bug più. Debug **Stati di sistema**.

### La tassa di serializzazione nessuno parla di

Ecco un costo che non viene quasi mai discusso nella difesa dei microservizi: **serializzazione e deserializzazione (SerDe)**.

Per "confine di servizio" si intende:

```csharp
// Monolith: direct object reference
var user = _userService.GetUser(userId);  // < 1μs
var tier = user.Tier;                      // Memory access

// Microservices: serialize → network → deserialize
var json = JsonSerializer.Serialize(user);        // ~50μs for a complex object
var bytes = Encoding.UTF8.GetBytes(json);         // ~10μs
// ... send over network ...
var responseJson = await response.Content.ReadAsStringAsync();  // ~20μs
var user = JsonSerializer.Deserialize<User>(responseJson);      // ~70μs
var tier = user.Tier;                                           // Finally
```

**Per chiamata, sono ~150μs di pura CPU sopra prima che la rete venga coinvolta.**

Ora moltiplicatelo attraverso una catena di chiamate:

* Servizio d'ordine deserializza la richiesta (150μs)
* Ordinare il servizio serializza la richiesta del servizio utente (150μs)
* Servizio utente deserializza la richiesta (150μs)
* Il servizio utente serializza la risposta (150μs)
* Servizio d'ordine deserializza la risposta dell'utente (150μs)
* Servizio d'ordine serializza la richiesta di Inventario (150μs)
* e così via

**In una catena di 5 servizi, si sta spendendo ~ 1.5m solo su SerDe** - e questa è serializzazione JSON ottimista. Se stai usando XML, Buffer di protocollo con riflessione, o serializzatori inefficienti, moltiplicalo per 2-10x.

Sì, si può ridurre questo con tecniche come [Generazione sorgente JSON in .NET](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation):

```csharp
[JsonSerializable(typeof(User))]
internal partial class UserJsonContext : JsonSerializerContext { }

// ~30-40% faster than reflection-based serialization
var json = JsonSerializer.Serialize(user, UserJsonContext.Default.User);
```

Ma stai ancora facendo serializzazione, hai appena fatto la tassa un po' piu' economica, in un monolito, la tassa e' zero.

#### Un vero esempio: la catastrofe di SerDe

Una volta ho profilato un sistema di ricerca degli indirizzi con questa architettura (tutti in contenitori Kubernetes):

```
ASP.NET API → Go Service (fan-out) → ASP.NET Search Service → Elasticsearch
```

Il servizio Go ha gestito il fan-out a più fonti di dati e risultati aggregati. Ogni richiesta significava serializzazione multipla attraverso i confini dei container.

**La ripartizione per una ricerca tipica indirizzo:**

* ASP.NET API: Serialize search request → JSON (80μs)
* Rete: ASP.NET → Vai (variabile per carico di cluster)
* Servizio Go: Deserialize richiesta (40μs)
* Servizio Go: Scendere - serializzare le richieste a più backend (60μs × N backend)
* Rete: Vai → Ricerca ASP.NET (varie)
* Servizio di ricerca: Deserialize richiesta (90μs)
* Servizio di ricerca: Build Elasticsearch query, serializza (120μs)
* Rete: Ricerca → Ricerca elastica (varie)
* Ricerca elastica: Deserializzare query, eseguire, serializzare i risultati (ricerca effettiva ~5ms, SerDe ~200μs)
* Rete: Elasticsearch → Ricerca (varie)
* Servizio di ricerca: Deserialize ES response (300μs), map to domain model, serializza (180μs)
* Rete: Cerca → Vai (varie)
* Servizio Go: deserializzare le risposte da tutti i backend (150μs × N), aggregato, serializzare (80μs)
* Rete: Vai → ASP.NET (varie)
* ASP.NET API: Deserializzare la risposta finale (120μs)

**Per un fan-out a 3 backend:**

* Totale SerDe spese generali: ~2ms prima Elasticsearch anche iniziato
* Rete aerea tra i container: ~2-3ms
* Ricerca effettiva + logica aziendale: ~6ms

**Stavamo passando quasi tanto tempo su SerDe quanto su una vera e propria ricerca.**

Il costo non era il servizio Go in sé - fan-out aveva senso per il caso di utilizzo. Il costo era il **limiti di servizio**. Ogni bordo contenitore significa serializzare → rete → deserializzare.

In un monolito che fa la stessa logica fan-out:

* Stesse query di ricerca elastica
* Stessa logica di aggregazione
* Zero inter-service SerDe (solo chiamate di metodo)
* ~40% meno latenza totale

Questo costo:

* Scala con complessità dell'oggetto (oggetti nidificati, collezioni, polimorfismi)
* Composti con profondità di chiamata (catene di servizio)
* Masterizza la CPU su ogni richiesta (non è possibile nasconderla)
* Aumenta la pressione GC (rifornimento da array string/byte)
* **Aggiunge veloce quando si sta facendo centinaia di richieste al secondo**

In un monolito, questo costo è zero. L'oggetto è già in memoria.

### Il mito delle prestazioni: throughput vs. scalabilità

Ecco un passo di vendita comune: "Microservices migliorare le prestazioni."

**Questo e' al contrario.**

I microservizi sono: **più lento** per le stesse risorse di calcolo. Cerchiamo di essere precisi su ciò che intendiamo:

**Flusso di cassa** (richieste al secondo per nucleo della CPU):

- **Monolito**: Più alto - zero SerDe, zero rete luppolo, chiamate di metodo diretto
- **Microservizi**: Lower - SerDe overhead, rete latenza, orchestrazione container

**Scalabilità** (capacità di gestire più richieste totali aggiungendo risorse):

- **Monolito**: Limitato da scala verticale (macchine più grandi)
- **Microservizi**: Orizzontale - aggiungere altre istanze di servizi di collo di bottiglia

I microservizi ti permettono **scala indipendentemente**. Se il vostro servizio di inventario ha bisogno di risorse 10x, ma il vostro servizio utente non, è possibile scalare separatamente. **quando ne hai bisogno**.

Ma non e' un'ottimizzazione delle prestazioni. **Strategia di scalatura**. E viene con una tassa.

#### I moderni monoliti possono scalare troppo

L'argomento "Microservices scale better" ha avuto più senso nel 2010 quando i server avevano 4-8 core.

I server moderni hanno 32-128 core. Una singola macchina può eseguire migliaia di operazioni contemporaneamente in modo efficiente.

Se il vostro monolito è costruito con modelli di esecuzione contemporanei moderni, può gestire il flusso massiccio su una singola distribuzione. Per esempio, utilizzando modelli come [Coordinatori di esecuzione effimeri](/blog/ephemeral-execution-library), puoi:

```csharp
// Process thousands of concurrent operations in a monolith
services.AddEphemeralWorkCoordinator<TranslationRequest>(
    async (request, ct) => await TranslateAsync(request, ct),
    new EphemeralOptions { MaxConcurrency = Environment.ProcessorCount * 4 });

// Bounded, observable, efficient concurrent processing
// No network overhead, no SerDe, no container orchestration
// Can handle 10k+ req/sec on a single machine
```

Questo ti dà:

- **Parallelismo**: Il lavoro si svolge contemporaneamente su tutti i core
- **Osservabilità**: Track opening/active/failed operations
- **Contropressione**: Le code chiuse impediscono l'esaurimento della memoria
- **Zero in testa**: Nessuna serializzazione, nessuna rete, nessun tracciamento distribuito

Quando tu **fare** necessità di scalare oltre una macchina, è possibile:

1. Eseguire più istanze dietro un bilanciere di carico (scala orizzontale senza stato)
2. Usare un pattern outbox per la distribuzione del lavoro asincrono
3. Partizione per chiave (ID cliente, regione, ecc.) tra le istanze

Stai ancora usando un monolito, l'hai appena dispiegato piu' volte.

**Il punto**: Non dividere in microservizi per "prestazioni." Dividere quando **scalamento indipendente di componenti specifici** giustifica la copertura operativa.

### Carico cognitivo

I microservizi non riducono la complessità, li ridistribuiscono.

Il "servizio semplice" ha ancora bisogno di contesto:

* Chi lo chiama?
* Cosa significa "corretto"?
* Cosa succede quando e' giu'?
* Qual e' il piano di rollback?
* Qual e' il grafico della dipendenza?

La gente sottovaluta questo perché i diagrammi lo nascondono.

### Versione e Drift Contract

Ogni confine diventa un contratto.  
Ogni contratto diventa:

* Regole di versione
* Garanzie di compatibilità
* Costrizioni sull'evoluzione dello schema
* Coordinamento in alto che hai fatto finta di non avere

Puoi evitarlo per un po' con "dispiegare tutto insieme."

Questa è la battuta: se dispiegate tutto insieme, avete costruito un monolito. Solo uno peggiore.

### Strumentazione prima del valore

La spesa iniziale è sempre la stessa:

* Scoperta del servizio
* Gestione dei segreti
* Tracciamento distribuito
* Registrazione centralizzata
* Metrics and alerting
* Modelli CI/CD
* Storia di dev locale che non è doloroso
* Gestione della dipendenza
* Un modo per correre tutto senza piangere

Nessuna di queste navi valore del prodotto.

E la maggior parte dei team lo fanno prima di aver dimostrato che il prodotto merita la complessità.

Questo è l'errore centrale: **paghi la tassa di cerimonia prima di aver guadagnato il valore**. Come [standup giornalieri che perdono tempo senza fornire allineamento](/blog/agile-standups-ceremony-tax), i microservizi possono diventare architettura rituale: impressionante da guardare, costoso da mantenere, e disconnesso dal problema che stai risolvendo.

Prendendo insieme, questi costi non scompaiono - si mescolano. E si mescolano se l'organizzazione ha bisogno di microservizi in primo luogo.

## Quello che la gente dimentica: gli umani e le squadre

L'architettura esiste per aiutare i gruppi di esseri umani a cambiare software in modo sicuro.

Non per impressionare gli altri ingegneri.  
Non per soddisfare un diagramma.  
Non per giustificare una squadra di piattaforme che non hai.

Le dimensioni del team e la cadenza di distribuzione contano più del catalogo dei pattern.

* Se una squadra possiede l'intera roadmap, un monolito di solito è più veloce e più sicuro.
* Se si capisce ancora il sistema end-to-end, i microservizi ridurranno questa chiarezza.
* Se non si hanno limiti di proprietà stabili, i microservizi forzeranno il coordinamento attraverso le API - che è il meccanismo di coordinamento più lento disponibile.

**La legge di Conway non e' facoltativa, e' fisica.**

La vostra architettura rifletterà la vostra struttura di comunicazione che vi piaccia o meno.

I microservizi non creano autonomia, ne hanno bisogno.

## In difesa del monolito modulare

La maggior parte dei sistemi dovrebbe iniziare come un monolito, ma non come uno sciattino.

A **monolito modulare** è:

* Un'unità di spiegamento
* Un runtime
* Un posto per il debug
* Una visione coerente dello stato
* Ma con veri confini interni

Significa:

* Moduli di dominio con dipendenze esplicite
* Cancella proprietà nel codice
* Niente "tutto fa riferimento a tutto"
* Contratti eseguiti all'interno della base di codici

Il punto non è rimanere monolitici per sempre. Il punto è quello di **guadagnare i confini prima di renderli remoti**.

Un monolito modulare ti costringe a imparare le abilità che i microservizi fingono di risolvere:

* Modellazione del dominio (confini reali, non confini delle cartelle)
* Separazione delle preoccupazioni (non "abbiamo fatto un servizio")
* Verificabilità
* Cambia disciplina
* Sapere cosa fa realmente il vostro sistema

**Se non riesci a costruire un monolito modulare pulito, i microservizi non ti salveranno.**

### Buon design monolith: modelli che scalano in seguito

Il disegno del monolite destro ti permette **rinviare la decisione relativa ai microservizi** senza chiuderti dentro.

#### Il modello di outbox: Async senza distribuzione

Uno dei migliori esempi è il **Schema outbox** - un modo per raggiungere l'eventuale coerenza e l'elaborazione asincrona *dentro* un monolito, con un percorso pulito verso la distribuzione in seguito.

Invece di:

```csharp
// Tightly coupled synchronous code
public async Task PlaceOrder(Order order)
{
    await _orderRepo.Save(order);
    await _emailService.SendConfirmation(order);  // Blocks on external service
    await _inventoryService.Reserve(order.Items); // Blocks on another service
}
```

Scrivi:

```csharp
// Outbox pattern: write events to a table
public async Task PlaceOrder(Order order)
{
    await using var transaction = await _db.Database.BeginTransactionAsync();
    
    // Save order
    await _orderRepo.Save(order);
    
    // Write events to outbox table (same transaction)
    _db.OutboxMessages.Add(new OutboxMessage
    {
        EventType = "OrderPlaced",
        Payload = JsonSerializer.Serialize(order),
        CreatedAt = DateTime.UtcNow
    });
    
    await _db.SaveChangesAsync();
    await transaction.CommitAsync();
    
    // Background worker picks up outbox events and publishes them
}
```

**Ciò che questo ti dà:**

* **Coerenza delle operazioni**: Eventi e dati si impegnano insieme o non si impegnano affatto
* **Disaccoppiamento**: La logica email/inventory funziona async, non blocca la creazione dell'ordine
* **Affidabilità**: Gli eventi persistono anche se i sistemi a valle sono in calo
* **Percorso di estrazione pulito**: Più tardi, scambiare il lavoratore outbox per Kafka/RabbitMQ senza cambiare la logica dell'ordine

La [Progetto campione SegmentCommerce](/blog/zero-pii-customer-intelligence-part2) dimostra questo modello di produzione:

```csharp
// Mostlylucid.SegmentCommerce/Services/Queue/PostgresOutbox.cs
public class PostgresOutbox
{
    public async Task PublishAsync<T>(string eventType, T payload)
    {
        var message = new OutboxMessage
        {
            EventType = eventType,
            Payload = JsonSerializer.Serialize(payload),
            CreatedAt = DateTime.UtcNow
        };
        
        _db.OutboxMessages.Add(message);
        // Caller commits transaction
    }
}

// Background service
public class OutboxProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var pending = await _db.OutboxMessages
                .Where(m => !m.Processed)
                .OrderBy(m => m.CreatedAt)
                .Take(100)
                .ToListAsync();
                
            foreach (var message in pending)
            {
                await ProcessMessage(message);
                message.Processed = true;
            }
            
            await _db.SaveChangesAsync();
            await Task.Delay(1000, stoppingToken);
        }
    }
}
```

Questo viene eseguito in una singola app oggi. Quando si ha bisogno di scalare:

1. Sostituire il lavoratore di background con un consumatore di bus di messaggio
2. Pubblica gli eventi outbox su Kafka/RabbitMQ invece di elaborare localmente
3. **Il codice di posizionamento dell'ordine non cambia**

Hai costruito un'infrastruttura pronta per i microservizi senza pagare la tassa di distribuzione.

#### Altri modelli vale la pena di fare presto

* **CQRS (luce)**: Modelli separati di lettura/scrittura anche se condividono un DB
* **Evento di approvvigionamento (selezionato)**: Solo per soggetti critici in materia di audit
* **Flag di funzionalità**: Dispiegamento del disaccoppiamento dal rilascio
* **Posti di lavoro di base**: Non bloccare le richieste, processa l'async

Nessuno di questi richiede la distribuzione. Tutti rendono la distribuzione più facile in seguito.

## Quando i microservizi effettivamente fanno il senso

I microservizi hanno senso quando i vincoli sono reali, non aspirazionali.

La domanda critica non è "dovremmo usare i microservizi?" E': **i benefici superano l'imposta operativa?**

Come scegliere tra [piccoli modelli locali e LLM di frontiera](/blog/small-models-not-budget-option), si tratta di capire dove appartiene la complessità e quali modalità di guasto si può permettersi.

### Buoni trigger

* **La contesa del team è misurabile**: Le squadre si bloccano a vicenda settimanalmente
* **Sono necessari schieramenti indipendenti**: La cadenza di rilascio differisce per dominio
* **La scala è irregolare**: Un sottosistema ha bisogno di 10x risorse e isolamento
* **Mancanza di isolamento**: Un componente non deve abbattere il resto
* **L'isolamento regolamentare/data è obbligatorio**: I confini non sono negoziabili
* **La struttura di org è già multi-squadra**: La proprietà esiste ed è stabile

### Trigger difettosi

* "Potremmo scalare"
* "Netflix lo fa"
* "Best practice"
* "Sarà bello"
* "Il nostro monolito è incasinato, quindi lo risolveremo dividendolo"

**Se la tua ragione contiene le parole *potrebbe*, *alla fine*, oppure *a prova di futuro*Probabilmente non e' un motivo.**

## La realtà tecnica: ciò che i microservizi effettivamente costano

Ecco cosa cambia quando dividi un monolito in servizi.

### Catene di chiamata e latenza

**Monolito:**

```csharp
public async Task<Order> PlaceOrder(OrderRequest request)
{
    var user = await _userService.GetUser(request.UserId);
    var inventory = await _inventoryService.CheckStock(request.Items);
    var price = _pricingService.Calculate(request.Items, user.Tier);
    
    var order = new Order { /* ... */ };
    await _orderRepository.Save(order);
    await _emailService.SendConfirmation(order);
    
    return order;
}
```

Latenza totale: ~50m (chiamate in corso + 2 domande DB)

**Microservizi:**

```csharp
public async Task<Order> PlaceOrder(OrderRequest request)
{
    // HTTP call to User Service (network + TLS + serialization)
    var user = await _httpClient.GetAsync<User>($"http://user-service/users/{request.UserId}");
    
    // HTTP call to Inventory Service
    var inventory = await _httpClient.PostAsync<StockResult>(
        "http://inventory-service/check", request.Items);
    
    // HTTP call to Pricing Service
    var price = await _httpClient.PostAsync<PriceResult>(
        "http://pricing-service/calculate", 
        new { items = request.Items, tier = user.Tier });
    
    // HTTP call to Order Service
    var order = await _httpClient.PostAsync<Order>(
        "http://order-service/orders", request);
    
    // Event published to message bus
    await _messageBus.Publish(new OrderPlaced { OrderId = order.Id });
    
    return order;
}
```

Latenza totale: ~400ms (anche ottimista 50 0,0680ms per chiamata HTTP in ambienti reali + bus di messaggio pubblica ~20ms)

**Cio' che hai guadagnato**

* Ogni servizio può essere distribuito in modo indipendente
* Inventario e prezzi possono scalare separatamente dagli ordini
* Il fallimento nell'email non blocca la creazione dell'ordine (evento async)

**Quello che hai pagato:**

* ~8x aumento della latenza
* 4 nuovi punti di guasto (qualsiasi servizio può essere giù/lento)
* Rete, DNS, TLS sopra ogni chiamata
* **Serialization/deserialization overhead** (vedere paragrafo precedente)
* Necessità di riprova logica, interruttori di circuito, timeout
* Tracciamento distribuito a errori di debug

### Errore nel gestire l'esplosione

**Gestione degli errori monoliti:**

```csharp
try
{
    var order = await PlaceOrder(request);
    return Ok(order);
}
catch (InsufficientStockException)
{
    return BadRequest(new { error = "Out of stock" });
}
catch (Exception ex)
{
    _logger.LogError(ex, "Order placement failed");
    return StatusCode(500);
}
```

**Gestione degli errori dei microservizi:**

```csharp
try
{
    // Call User Service
    var user = await _httpClient.GetAsync<User>($"http://user-service/users/{request.UserId}");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    return NotFound(new { error = "User not found" });
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)
{
    // Retry with exponential backoff?
    // Circuit breaker opened?
    // Fail fast or degrade gracefully?
    _logger.LogWarning("User service unavailable, retrying...");
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    // ... retry logic ...
}
catch (TaskCanceledException ex)
{
    // Timeout - was the request processed? Do we retry?
    _logger.LogError("User service timeout");
    return StatusCode(503, new { error = "Service temporarily unavailable" });
}

try
{
    var inventory = await _httpClient.PostAsync<StockResult>(
        "http://inventory-service/check", request.Items);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
{
    // Business error from remote service
    var errorDetails = await ex.Content.ReadAsAsync<ErrorResponse>();
    return BadRequest(new { error = errorDetails.Message });
}
catch (HttpRequestException ex)
{
    // Which service failed? Network issue? Service down?
    // Do we have a fallback? Cached data? Fail fast?
    _logger.LogError(ex, "Inventory service failed");
    // Maybe try a backup instance?
    // ... more retry logic ...
}

// And repeat for every service call...
```

Ogni chiamata di rete introduce:

* Scenari di timeout
* Errore di rete
* indisponibilità del servizio
* Fallimenti parziali (richiesta inviata, risposta non ricevuta)
* Riprova l'amplificazione (la tua riprova potrebbe innescare la loro riprova)
* Incubi di transazione distribuiti

### Coordinamento dell'occupazione

**Distribuzione di monoliti:**

```bash
# Build
dotnet publish -c Release

# Run migrations
dotnet ef database update

# Deploy
docker push myapp:v1.2.3
kubectl set image deployment/myapp myapp=myapp:v1.2.3

# Rollback if needed
kubectl rollout undo deployment/myapp
```

**Dispiegamento di microservizi:**

Stai cambiando la struttura dell'ordine. `Order` include `UserTier` direttamente (denormalizzato per le prestazioni).

```bash
# 1. Deploy Pricing Service v2 (understands both old and new Order format)
kubectl set image deployment/pricing-service pricing-service=pricing:v2.0.0

# 2. Wait for rollout and monitor
kubectl rollout status deployment/pricing-service
# Check metrics - is v2 working with old order format?

# 3. Deploy Order Service v2 (starts sending new format)
kubectl set image deployment/order-service order-service=order:v2.0.0

# 4. Monitor for errors
# If Pricing v2 has a bug, you can't just rollback Order service
# You have to rollback both in reverse order

# 5. Deploy User Service v2 (stops including tier in response)
# But wait - old Order Service instances might still be running!
# Need zero-downtime rollout or maintain backward compat

# 6. Eventually clean up old code paths in Pricing Service v3
# (6 months later because you're scared to remove backward compat)
```

Con i microservizi, ogni cambiamento attraversa molteplici servizi. L'evoluzione del contratto richiede:

* Finestre di compatibilità all'indietro
* Flag di funzionalità tra i servizi
* Implementazioni coordinate
* Matrici di prova estese (Service A v1 + Service B v2, Service A v2 + Service B v1, ecc.)

Niente di tutto questo è impossibile, ma richiede disciplina, strumenti ed esperienza che la maggior parte delle squadre guadagnano solo dopo anni di dolore.

### L'esperienza di debug

**Segnalazione bug monolith:** "Il posizionamento dell'ordine fallisce per gli utenti premium"

```csharp
// Set breakpoint in PlaceOrder
// Step through each call
// User tier is null - ah, there's the bug
// Fix, test, deploy
```

**Segnalazione bug Microservices:** "Il posizionamento dell'ordine fallisce per gli utenti premium"

```bash
# 1. Check logs - which service failed?
kubectl logs -l app=order-service --tail=100

# 2. Oh, it's calling Pricing service. Check those logs
kubectl logs -l app=pricing-service --tail=100

# 3. Grep for correlation ID across all services
stern -l app=order-service,app=pricing-service,app=user-service \
  | grep "correlation-id-xyz"

# 4. Reconstruct the call chain from distributed traces
# Open Jaeger/Zipkin, find the trace
# User service returned 200 OK
# Pricing service returned 400 Bad Request
# Error: "user.tier is required"

# 5. Check User service - did it send tier?
# Look at schema version - ah, User v1.2 stopped sending tier
# When did that deploy? Was Pricing service updated?

# 6. Check API contracts
# Pricing expects tier, User stopped sending it
# Who approved this change?

# 7. Fix requires coordinating two teams
# Can't just deploy a fix - need contract negotiation
```

Questa è la realtà: **bug che erano correzioni di 5 minuti diventano indagini multi-squadra**.

Se questo suona estremo, bene. I microservizi sono ingegneria estrema.

## Come evolvere in modo sicuro: Monolith → Microservices

I microservizi sono una porta a senso unico.

Il sentiero sicuro sembra noioso:

### 1. Dimostrare prima i confini

Se non è possibile tracciare il confine all'interno del monolito (con dipendenze esecutive), non è possibile estrarlo in modo sicuro.

Ottenere le cuciture a destra localmente prima.

### 2. Estrarre le letture prima di scrivere

Le letture sono più facili:

* Meno invarianti
* Meno requisiti di coerenza
* Meno incubi di rollback

Sposta prima i modelli di lettura se hai bisogno di separazione.

### 3. Preferire l'asincronia prima della sincronizzazione

Le chiamate di servizio sincronizzate creano catene di dipendenza.

La messaggistica async crea:

* BufferingCity name (optional, probably does not need a translation)
* Resilienza
* Il disaccoppiamento è possibile sopravvivere

Inizia separando eventi e fatti, non tagliando gli endpoint.

### 4. Strangolare, non riscrivere

Niente gran botto.  
Niente "noi lo riscriviamo correttamente."

Tagliare il contorno, misurarlo, possederlo, e continuare.

**Se non puoi spiegare perché un servizio esiste indipendentemente, non dovrebbe esistere affatto.**

## Il Takeaway: Microservices paga solo se si capisce il commercio-Offs

I microservizi non sono un punto di partenza, ma un risultato.

**La complessità dovrebbe essere guadagnata.** I sistemi distribuiti non sono un distintivo - sono uno strumento di debito.

L'equazione trade-off è semplice:

```
Microservices Value = (Team Autonomy + Independent Scaling + Failure Isolation)
                     - (Latency Tax + SerDe Tax + Operational Overhead + Coordination Cost)
```

Per la maggior parte dei team - in particolare i prodotti per la prima fase o per la singola squadra - domina il lato giusto. Si pagano costi enormi per i benefici che non hai ancora bisogno.

Ma quando l'hai fatto

* 5+ squadre che si calpestano l'un l'altro
* Coda di distribuzione misurata in giorni
* Sottosistemi con esigenze di scalatura estremamente diverse
* Requisiti normativi per l'isolamento dei dati

...allora il lato sinistro comincia a vincere. L'imposta diventa giustificata.

### Un quadro decisionale

Fate queste domande in ordine:

1. **Una squadra puo' ancora possedere questo end-to-end?**  
   → Sì: rimanere monolito. No: considerare la divisione.

2. **Le squadre sono bloccate dai cicli di rilascio dell'altro?**  
   → Sì: i microservizi potrebbero aiutare. No: il coordinamento sta funzionando.

3. **Abbiamo il muscolo operativo per far funzionare i sistemi distribuiti?**  
   → No: costruirlo prima. Sì: procedere con attenzione.

4. **Possiamo rintracciare, debug e distribuire più servizi senza eroismi?**  
   → No: non sei pronto. Sì: potresti esserlo.

5. **Abbiamo prima provato i limiti del codice?**  
   → No: riparate la struttura del monolito. Sì: l'estrazione è più sicura.

Se non riesci a superare la domanda 3 con fiducia, non sei pronto per i microservizi - e va bene. **L'architettura noiosa è un vantaggio competitivo.**

La maggior parte dei prodotti di successo sono stati costruiti come monoliti prima: GitHub, Shopify, Stack Overflow, Basecamp. Si sono evoluti in sistemi distribuiti solo quando il dolore organizzativo richiesto.

Il vostro compito non è quello di costruire l'architettura più impressionante. È quello di fornire valore minimizzando la complessità accidentale.

E nessun cliente ha mai pagato extra perché il vostro sistema ha usato Kafka.

A volte questo significa microservizi. Di solito significa un monolito ben strutturato e la disciplina per mantenerlo in questo modo.