Costruire microservizi guidati dal dominio

Chandra Ramalingam

Follow

1 luglio, 2020 – 18 min read

Image credits:

Il termine ‘micro’ in Microservices, anche se indicativo delle dimensioni di un servizio, non è l’unico criterio che rende un’applicazione un Microservice. Quando i team passano a un’architettura basata sui microservizi, mirano ad aumentare la loro agilità – distribuire caratteristiche in modo autonomo e frequente. È difficile trovare una singola definizione concisa di questo stile architettonico. Mi è piaciuta questa breve definizione di Adrian Cockcroft – “architettura orientata ai servizi composta da elementi loosely coupled che hanno contesti delimitati.”

Anche se questo definisce un’euristica di design di alto livello, l’architettura dei microservizi ha alcune caratteristiche uniche che la differenziano dall’architettura orientata ai servizi di un tempo. Alcune di queste caratteristiche, qui sotto. Queste e alcune altre sono ben documentate – l’articolo di Martin Fowler e Sam Newman Building Microservices, per nominarne alcune.

  1. I servizi hanno confini ben definiti centrati sul contesto di business, e non su astrazioni tecniche arbitrarie
  2. Nascondono i dettagli di implementazione ed espongono le funzionalità attraverso interfacce che rivelano le intenzioni
  3. I servizi non condividono le loro strutture interne oltre i loro confini. Per esempio, nessuna condivisione di database.
  4. I servizi sono resistenti ai fallimenti.
  5. Le squadre possiedono le loro funzioni in modo indipendente e hanno la capacità di rilasciare cambiamenti autonomamente
  6. Le squadre abbracciano una cultura di automazione. Per esempio, test automatizzati, integrazione continua e consegna continua

In breve, possiamo riassumere questo stile di architettura come segue:

Architettura orientata ai servizi liberamente accoppiati, dove ogni servizio è racchiuso in un contesto ben definito, permettendo una consegna rapida, frequente e affidabile delle applicazioni.

Domain-driven design e Bounded contexts

Il potere dei microservizi deriva dal definire chiaramente la loro responsabilità e dal delimitare il confine tra di loro. Lo scopo qui è di costruire un’alta coesione all’interno del confine e un basso accoppiamento al di fuori di esso. Cioè, le cose che tendono a cambiare insieme dovrebbero stare insieme. Come in molti problemi della vita reale, questo è più facile a dirsi che a farsi – le aziende si evolvono, e le ipotesi cambiano. Quindi la capacità di rifattorizzare è un’altra cosa critica da considerare quando si progettano i sistemi.

Il design guidato dal dominio (DDD) è una chiave, e a nostro parere, uno strumento necessario quando si progettano microservizi, sia che si tratti di rompere un monolite o di implementare un progetto greenfield. Il design guidato dal dominio, reso famoso da Eric Evans con il suo libro, è un insieme di idee, principi e modelli che aiutano a progettare sistemi software basati sul modello sottostante del dominio aziendale. Gli sviluppatori e gli esperti di dominio lavorano insieme per creare modelli di business in un linguaggio comune Ubiquitous. Poi legano questi modelli ai sistemi dove hanno senso, stabiliscono protocolli di collaborazione tra questi sistemi e i team che lavorano su questi servizi. Ancora più importante, progettano i contorni concettuali o i confini tra i sistemi.

Il design dei microservizi trae ispirazione da questi concetti poiché tutti questi principi aiutano a costruire sistemi modulari che possono cambiare ed evolvere indipendentemente l’uno dall’altro.

Prima di procedere oltre, esaminiamo velocemente alcune delle terminologie di base del DDD. Una panoramica completa del Domain-Driven Design è fuori dallo scopo di questo blog. Consigliamo vivamente il libro di Eric Evans a chiunque cerchi di costruire microservizi

Dominio: Rappresenta ciò che fa un’organizzazione. Nell’esempio qui sotto, sarebbe Retail o eCommerce.

Subdomain: Un’organizzazione o un’unità di business all’interno di un’organizzazione. Un dominio è composto da più sottodomini.

Linguaggio universale: Questo è il linguaggio usato per esprimere i modelli. Nell’esempio qui sotto, Item è un modello che appartiene al linguaggio Ubiquitous di ciascuno di questi sottodomini. Sviluppatori, Product Manager, esperti di dominio e stakeholder di business sono d’accordo sullo stesso linguaggio e lo usano nei loro artefatti – codice, documentazione di prodotto e così via.

Fig 1. Sottodomini e contesti limitati nel dominio eCommerce

Contesti limitati: Domain-driven design definisce Bounded contexts come “L’ambiente in cui appare una parola o un’affermazione che determina il suo significato”. In breve, questo significa il confine entro il quale un modello ha senso. Nell’esempio precedente, “Item” assume un significato diverso in ciascuno di questi contesti. Nel contesto Catalogo, un Item significa un prodotto vendibile, mentre, nel contesto Carrello, significa l’articolo che il cliente ha aggiunto al suo carrello. Nel contesto Fulfillment, significa un articolo di magazzino che sarà spedito al cliente. Ognuno di questi modelli è diverso, e ognuno ha un significato diverso e possibilmente contiene attributi diversi. Separando e isolando questi modelli all’interno dei loro rispettivi confini, possiamo esprimere i modelli liberamente e senza ambiguità.

Nota: è essenziale capire la distinzione tra sottodomini e contesti limitati. Un sottodominio appartiene allo spazio del problema, cioè come la vostra azienda vede il problema, mentre i contesti limitati appartengono allo spazio della soluzione, cioè come implementeremo la soluzione al problema. Teoricamente, ogni sottodominio può avere più contesti vincolati, anche se noi ci sforziamo di avere un contesto vincolato per sottodominio.

Come sono collegati i microservizi ai contesti vincolati

Ora, dove stanno i microservizi? È giusto dire che ogni contesto delimitato corrisponde a un microservizio? Sì e no. Vedremo perché. Ci possono essere casi in cui il confine o il contorno del vostro contesto delimitato è abbastanza grande.

Fig 2. Contesto delimitato e microservizi

Consideriamo l’esempio precedente. Il contesto vincolato Pricing ha tre modelli distinti – Price, Priced items, e Discounts, ciascuno responsabile rispettivamente del prezzo di un articolo del catalogo, del calcolo del prezzo totale di un elenco di articoli, e dell’applicazione di sconti. Potremmo creare un unico sistema che comprenda tutti i modelli di cui sopra, ma potrebbe diventare un’applicazione irragionevolmente grande. Ognuno dei modelli di dati, come menzionato prima, ha le sue invarianti e regole di business. Nel tempo, se non stiamo attenti, il sistema potrebbe diventare una grande palla di fango con confini oscurati, responsabilità sovrapposte, e probabilmente tornare al punto di partenza – un monolite.

Un altro modo per modellare questo sistema è quello di separare, o raggruppare i modelli correlati in microservizi separati. In DDD, questi modelli – Price, Priced Items, e Discounts – sono chiamati aggregati. Un aggregato è un modello autonomo che compone modelli correlati. Si può cambiare lo stato di un aggregato solo attraverso un’interfaccia pubblicata, e l’aggregato assicura la coerenza e che le invarianti tengano bene.

Formalmente, un aggregato è un gruppo di oggetti associati trattati come un’unità per le modifiche dei dati. I riferimenti esterni sono limitati a un membro dell’AGGREGATO, designato come la radice. Un insieme di regole di coerenza si applica entro i confini dell’AGGREGATE.

Fig 3. Microservizi nel contesto dei prezzi

Ancora una volta, non è necessario modellare ogni aggregato come un microservizio distinto. È risultato essere così per i servizi (aggregati) in Fig 3, ma questa non è necessariamente una regola. In alcuni casi, può avere senso ospitare più aggregati in un singolo servizio, in particolare quando non comprendiamo completamente il dominio del business. Una cosa importante da notare è che la coerenza può essere garantita solo all’interno di un singolo aggregato, e gli aggregati possono essere modificati solo attraverso l’interfaccia pubblicata. Qualsiasi violazione di queste regole comporta il rischio di trasformarsi in una grande palla di fango.

Mappe di contesto – Un modo per ritagliare accuratamente i confini dei microservizi

Un altro strumento essenziale nel vostro arsenale è il concetto di mappe di contesto – di nuovo, dal Domain Driven Design. Un monolite è di solito composto da modelli disparati, per lo più strettamente accoppiati – i modelli forse conoscono i dettagli intimi l’uno dell’altro, cambiare uno potrebbe causare effetti collaterali su un altro, e così via. Mentre si scompone il monolite, è vitale identificare questi modelli – aggregati in questo caso – e le loro relazioni. Le mappe di contesto ci aiutano a fare proprio questo. Sono usate per identificare e definire le relazioni tra i vari contesti delimitati e gli aggregati. Mentre i contesti delimitati definiscono il confine di un modello – Prezzo, Sconti, ecc. nell’esempio precedente, le mappe di contesto definiscono le relazioni tra questi modelli e tra diversi contesti. Dopo aver identificato queste dipendenze, possiamo determinare il giusto modello di collaborazione tra i team che implementeranno questi servizi.

Un’esplorazione completa delle mappe di contesto è oltre lo scopo di questo blog, ma la illustreremo con un esempio. Il diagramma seguente rappresenta le varie applicazioni che gestiscono i pagamenti per un ordine eCommerce.

  1. Il contesto del carrello si occupa delle autorizzazioni online di un ordine; il contesto dell’ordine elabora i processi di pagamento successivi all’evasione come la liquidazione; il centro di contatto gestisce tutte le eccezioni come riprovare i pagamenti e cambiare il metodo di pagamento usato per l’ordine
  2. Per semplicità, assumiamo che tutti questi contesti siano implementati come servizi separati
  3. Tutti questi contesti incapsulano lo stesso modello.
  4. Nota che questi modelli sono logicamente gli stessi. Cioè, seguono tutti lo stesso linguaggio di dominio Ubiquitous – metodi di pagamento, autorizzazioni e regolamenti. Solo che fanno parte di contesti diversi.

Un altro segno che lo stesso modello è diffuso in diversi contesti è che tutti questi si integrano direttamente con un unico gateway di pagamento e fanno le stesse operazioni l’uno dell’altro

Fig 4. Una mappa di contesto erroneamente definita

Rifinire i confini del servizio – Mappare gli aggregati ai contesti giusti

Ci sono alcuni problemi che sono molto evidenti nel disegno sopra (Fig 4). L’aggregato dei pagamenti fa parte di più contesti. È impossibile far rispettare le invarianti e la coerenza tra i vari servizi, per non parlare dei problemi di concorrenza tra questi servizi. Per esempio, cosa succede se il centro di contatto cambia il metodo di pagamento associato all’ordine mentre il servizio Ordini sta cercando di pubblicare la liquidazione di un metodo di pagamento precedentemente inviato. Inoltre, si noti che qualsiasi cambiamento nel gateway di pagamento forzerebbe i cambiamenti a più servizi e potenzialmente a numerosi team, poiché diversi gruppi potrebbero possedere questi contesti.

Con alcune modifiche e allineando gli aggregati ai contesti giusti, otteniamo una rappresentazione molto migliore di questi sottodomini – Fig 5. Ci sono molte cose che sono cambiate. Rivediamo i cambiamenti:

  1. L’aggregato Payment ha una nuova casa – Payment service. Questo servizio astrae anche il gateway di pagamento dagli altri servizi che richiedono servizi di pagamento. Poiché un singolo contesto delimitato ora possiede un aggregato, le invarianti sono facili da gestire; tutte le transazioni avvengono all’interno dello stesso confine del servizio, aiutando ad evitare qualsiasi problema di concorrenza.
  2. L’aggregato Payment usa un Anti-corruption Layer (ACL) per isolare il modello di dominio principale dal modello di dati del gateway di pagamento, che di solito è un fornitore terzo e forse destinato a cambiare. Ci immergeremo più a fondo nella progettazione dell’applicazione di tale servizio utilizzando il pattern Ports and Adapters in un prossimo post. Il livello ACL di solito contiene gli adattatori che trasformano il modello di dati del gateway di pagamento nel modello di dati aggregato Payments.
  3. Il servizio Cart chiama il servizio Payments attraverso chiamate API dirette, poiché il servizio Cart potrebbe dover completare l’autorizzazione al pagamento mentre i clienti sono sul sito web
  4. Si noti l’interazione tra il servizio Ordini e il servizio Payment. Il servizio Ordini emette un evento di dominio (più avanti in questo blog). Il servizio Payments ascolta questo evento e completa il regolamento dell’ordine
  5. Il servizio Contact Center può avere molti aggregati, ma noi siamo interessati solo all’aggregato Orders per questo caso d’uso. Questo servizio emette un evento quando il metodo di pagamento cambia, e il servizio Payments reagisce ad esso invertendo la carta di credito usata prima e processando la nuova carta di credito.

Fig 5. Mappa di contesto ridefinita

Di solito, un’applicazione monolitica o legacy ha molti aggregati, spesso con confini sovrapposti. Creare una mappa di contesto di questi aggregati e delle loro dipendenze ci aiuta a capire i contorni di qualsiasi nuovo microservizio che strapperemo da questi monoliti. Ricordate, il successo o il fallimento dell’architettura dei microservizi dipende dal basso accoppiamento tra gli aggregati e dall’alta coesione all’interno di questi aggregati.

È anche importante notare che i contesti delimitati sono essi stessi unità coesive adeguate. Anche se un contesto ha più aggregati, l’intero contesto, insieme ai suoi aggregati, può essere composto in un singolo microservizio. Troviamo questa euristica particolarmente utile per i domini che sono un po’ oscuri – pensate ad una nuova linea di business in cui l’organizzazione si sta avventurando. Si potrebbe non avere una comprensione sufficiente dei giusti confini di separazione, e qualsiasi decomposizione prematura degli aggregati può portare a costosi refactoring. Immaginate di dover fondere due database in uno solo, insieme alla migrazione dei dati, perché ci è capitato di scoprire che due aggregati appartengono insieme. Ma assicuratevi che questi aggregati siano sufficientemente isolati attraverso interfacce in modo che non conoscano i dettagli intricati l’uno dell’altro.

Event Storming – Un’altra tecnica per identificare i confini dei servizi

Event Storming è un’altra tecnica essenziale per identificare gli aggregati (e quindi i microservizi) in un sistema. È uno strumento utile sia per rompere i monoliti che quando si progetta un complesso ecosistema di microservizi. Abbiamo usato questa tecnica per rompere una delle nostre applicazioni complesse, e abbiamo intenzione di coprire le nostre esperienze con Event Storming in un blog separato. Per lo scopo di questo blog, vogliamo dare una rapida panoramica di alto livello. Guardate il video di Alberto Brandelloni sull’argomento se siete interessati ad approfondire.

In poche parole, l’Event Storming è un esercizio di brainstorming tra i team che lavorano su un’applicazione – nel nostro caso, un monolite – per identificare i vari eventi e processi di dominio che accadono all’interno di un sistema. I team identificano anche gli aggregati o i modelli che questi eventi influenzano e qualsiasi impatto successivo. Mentre i team fanno questo esercizio, identificano diversi concetti che si sovrappongono, un linguaggio di dominio ambiguo e processi di business in conflitto. Raggruppano i modelli correlati, ridefiniscono gli aggregati e identificano i processi duplicati. Man mano che procedono con questo esercizio, i contesti delimitati a cui appartengono questi aggregati diventano chiari. I workshop di Event Storming sono utili se tutti i team sono in una singola stanza – fisica o virtuale – e iniziano a mappare gli eventi, i comandi e i processi su una lavagna in stile scrum. Alla fine di questo esercizio, ecco i soliti risultati:

  1. Lista ridefinita di aggregati. Questi potenzialmente diventano nuovi microservizi
  2. Eventi di dominio che devono fluire tra questi microservizi
  3. Comandi che sono invocazioni dirette da altre applicazioni o utenti

Di seguito abbiamo mostrato un esempio di lavagna alla fine di un workshop Event Storming. È un grande esercizio collaborativo per i team per concordare i giusti aggregati e contesti delimitati. Oltre ad essere un ottimo esercizio di team-building, i team escono da questa sessione con una comprensione condivisa del dominio, un linguaggio ubiquo e precisi confini di servizio.

Fig 6. Scheda Event Storming

Comunicazione tra microservizi

Per ricapitolare rapidamente, un monolite ospita più aggregati all’interno di un unico confine di processo. Quindi è possibile gestire la coerenza degli aggregati all’interno di questo confine. Per esempio, se un cliente fa un ordine, possiamo diminuire l’inventario degli articoli, inviare un’email al cliente – tutto all’interno di una singola transazione. Tutte le operazioni avrebbero successo, o tutte fallirebbero. Ma, man mano che rompiamo il monolite e diffondiamo gli aggregati in diversi contesti, avremo decine o addirittura centinaia di microservizi. I processi che finora esistevano all’interno del singolo confine di un monolite sono ora distribuiti su più sistemi distribuiti. Raggiungere l’integrità transazionale e la coerenza in tutti questi sistemi distribuiti è molto difficile, e ha un costo – la disponibilità dei sistemi.

Anche i microservizi sono sistemi distribuiti. Quindi, il teorema CAP si applica anche a loro – “un sistema distribuito può fornire solo due delle tre caratteristiche desiderate: consistenza, disponibilità e tolleranza alle partizioni (le “C”, “A” e “P” di CAP). Nei sistemi del mondo reale, la tolleranza alle partizioni non è negoziabile – la rete non è affidabile, le macchine virtuali possono andare giù, la latenza tra le regioni può peggiorare, e così via.

Quindi questo ci lascia con una scelta di Disponibilità o Consistenza. Ora, sappiamo che in qualsiasi applicazione moderna, sacrificare la disponibilità non è una buona idea.

Fig 7. Teorema CAP

Progettare applicazioni intorno alla consistenza finale

Se si cerca di costruire transazioni su diversi sistemi distribuiti, si finirà di nuovo nella terra dei monoliti. Solo che questa volta sarà del tipo peggiore, un monolite distribuito. Se uno qualsiasi dei sistemi diventa indisponibile, l’intero processo diventa indisponibile, spesso portando ad un’esperienza frustrante per il cliente, promesse non mantenute, e così via. Inoltre, le modifiche a un servizio possono di solito comportare modifiche a un altro servizio, portando a implementazioni complesse e costose. Quindi, è meglio progettare le applicazioni adattando i nostri casi d’uso per tollerare un po’ di incoerenza a favore della disponibilità. Per l’esempio di cui sopra, possiamo rendere tutti i processi asincroni e quindi alla fine coerenti. Possiamo inviare e-mail in modo asincrono, indipendentemente dagli altri processi; se un articolo promesso non è disponibile nel magazzino più tardi, l’articolo potrebbe essere riordinato, o potremmo smettere di prendere ordini per l’articolo oltre una certa soglia.
Occasionalmente, si può incontrare uno scenario che potrebbe richiedere forti transazioni in stile ACID attraverso due aggregati in diversi confini di processo. Questo è un ottimo segno per rivedere questi aggregati e forse combinarli in uno solo. L’Event Storming e le mappe di contesto aiuteranno a identificare queste dipendenze all’inizio, prima di iniziare a suddividere questi aggregati in diversi confini di processo. Fondere due microservizi in uno è costoso, ed è qualcosa che dovremmo cercare di evitare.

Favorire l’architettura event-driven

I microservizi possono emettere cambiamenti essenziali che accadono ai loro aggregati. Questi sono chiamati eventi di dominio, e ogni servizio che è interessato a questi cambiamenti può ascoltare questi eventi e prendere le rispettive azioni all’interno dei loro domini. Questo metodo evita qualsiasi accoppiamento comportamentale – un dominio non prescrive ciò che gli altri domini dovrebbero fare, e l’accoppiamento temporale – il buon completamento di un processo non dipende dal fatto che tutti i sistemi siano disponibili allo stesso tempo. Questo, naturalmente, significa che i sistemi saranno alla fine coerenti.

Fig 8. Architettura guidata dagli eventi

Nell’esempio sopra, il servizio Ordini pubblica un evento – Ordine annullato. Gli altri servizi che hanno sottoscritto l’evento elaborano le loro rispettive funzioni di dominio: Il servizio Payment rimborsa il denaro, il servizio Inventory regola l’inventario degli articoli, e così via. Poche cose da notare per assicurare l’affidabilità e la resilienza di questa integrazione:

  1. I produttori dovrebbero assicurarsi di produrre un evento almeno una volta. Se ci sono fallimenti nel farlo, dovrebbero assicurarsi che ci sia un meccanismo di ripiego presente per far scattare nuovamente gli eventi
  2. I consumatori dovrebbero assicurarsi di consumare gli eventi in modo idempotente. Se lo stesso evento si verifica di nuovo, non ci dovrebbe essere alcun effetto collaterale alla fine del consumatore. Gli eventi possono anche arrivare fuori sequenza. I consumatori possono usare campi Timestamp o numeri di versione per garantire l’unicità degli eventi.

Può non essere sempre possibile usare l’integrazione basata sugli eventi a causa della natura di alcuni casi d’uso. Dai un’occhiata all’integrazione tra il servizio Cart e il servizio Payment. È un’integrazione sincrona, e quindi ha alcune cose a cui dovremmo prestare attenzione. È un esempio di accoppiamento comportamentale – il servizio Cart forse chiama un’API REST dal servizio Payment e gli ordina di autorizzare il pagamento per un ordine, e di accoppiamento temporale – il servizio Payment deve essere disponibile per il servizio Cart per accettare un ordine. Questo tipo di accoppiamento riduce l’autonomia di questi contesti e forse una dipendenza indesiderata. Ci sono alcuni modi per evitare questo accoppiamento, ma con tutte queste opzioni, perderemo la capacità di fornire un feedback immediato ai clienti.

  1. Convertire l’API REST in un’integrazione basata sugli eventi. Ma questa opzione potrebbe non essere disponibile se il servizio di pagamento espone solo un’API REST
  2. Il servizio Cart accetta un ordine istantaneamente, e c’è un lavoro batch che raccoglie gli ordini e chiama l’API del servizio di pagamento
  3. Il servizio Cart produce un evento locale che poi chiama l’API del servizio di pagamento

Una combinazione di quanto sopra con i tentativi in caso di fallimento e indisponibilità della dipendenza a monte – il servizio di pagamento – può risultare in un progetto molto più resiliente. Per esempio, l’integrazione sincrona tra i servizi Cart e Payment può essere sostenuta da un evento o da ritentativi basati su batch in caso di guasti. Questo approccio ha un impatto aggiuntivo sull’esperienza del cliente – i clienti potrebbero aver inserito dettagli di pagamento errati, e non li avremo online quando processeremo i pagamenti offline. Oppure potrebbe esserci un costo aggiuntivo per l’azienda per recuperare i pagamenti falliti. Ma con ogni probabilità, i benefici del servizio Carrello che è resiliente all’indisponibilità o ai guasti del servizio di pagamento superano i difetti. Per esempio, possiamo avvisare i clienti se non siamo in grado di raccogliere i pagamenti offline. In breve, ci sono compromessi tra l’esperienza dell’utente, la resilienza e i costi operativi, ed è saggio progettare i sistemi tenendo a mente questi compromessi.

Evitare l’orchestrazione tra i servizi per i bisogni di dati specifici dei consumatori

Uno degli anti-pattern in qualsiasi architettura orientata ai servizi è che i servizi si adattano ai modelli di accesso specifici dei consumatori. Di solito, questo accade quando i team dei consumatori lavorano a stretto contatto con i team dei servizi. Se il team stesse lavorando su un’applicazione monolitica, spesso creerebbe una singola API che attraversa diversi confini di aggregati, quindi accoppiando strettamente questi aggregati. Consideriamo un esempio. Diciamo che la pagina dei dettagli dell’ordine nelle applicazioni web e mobile ha bisogno di mostrare i dettagli di un ordine e i dettagli dei rimborsi elaborati per l’ordine su una singola pagina. In un’applicazione monolitica, un’API Order GET – assumendo che sia un’API REST – interroga Ordini e Rimborsi insieme, consolida entrambi gli aggregati e invia una risposta composita a chi chiama. È possibile fare questo senza molto overhead poiché gli aggregati appartengono allo stesso confine di processo. Così, i consumatori possono ottenere tutti i dati necessari in una singola chiamata.

Se Ordini e Rimborsi fanno parte di contesti diversi, i dati non sono più presenti all’interno di un singolo microservizio o confine aggregato. Un’opzione per mantenere la stessa funzionalità per i consumatori è rendere il servizio Ordini responsabile della chiamata del servizio Rimborsi e creare una risposta composita. Questo approccio causa diverse preoccupazioni:

1. Il servizio Order ora si integra con un altro servizio solo per supportare i consumatori che hanno bisogno dei dati Refunds insieme ai dati Order. Il servizio d’ordine è meno autonomo ora, poiché qualsiasi cambiamento nell’aggregato Refunds porterà a un cambiamento nell’aggregato Order.

2. Il servizio d’ordine ha un’altra integrazione e quindi un altro punto di fallimento da prendere in considerazione – se il servizio Refunds è giù, il servizio d’ordine può ancora inviare dati parziali, e i consumatori possono fallire con grazia?

3. Se i consumatori hanno bisogno di un cambiamento per recuperare più dati dall’aggregato Refunds, due team sono coinvolti ora per fare questo cambiamento

4. Questo schema, se seguito in tutta la piattaforma, può portare a un’intricata rete di dipendenze tra i vari servizi di dominio, tutto perché questi servizi soddisfano i modelli di accesso specifici dei chiamanti.

Backend for Frontends (BFFs)

Un approccio per mitigare questo rischio è lasciare che i team dei consumatori gestiscano l’orchestrazione tra i vari servizi di dominio. Dopo tutto, i chiamanti conoscono meglio gli schemi di accesso e possono avere il controllo completo di qualsiasi modifica a questi schemi. Questo approccio disaccoppia i servizi di dominio dal presentation tier, lasciando che si concentrino sui processi di core business. Ma se le applicazioni web e mobili iniziano a chiamare direttamente diversi servizi invece di un’unica API composita dal monolite, può causare un sovraccarico di prestazioni a queste applicazioni – chiamate multiple su reti a bassa larghezza di banda, elaborazione e fusione di dati da diverse API, e così via.

Invece, si potrebbe usare un altro pattern chiamato Backend per Front-ends. In questo pattern di progettazione, un servizio di backend creato e gestito dai consumatori – in questo caso, i team web e mobile – si occupa dell’integrazione tra più servizi di dominio solo per rendere l’esperienza front-end ai clienti. I team web e mobile possono ora progettare i contratti di dati in base ai casi d’uso a cui si rivolgono. Possono anche usare GraphQL invece delle API REST per interrogare in modo flessibile e ottenere esattamente ciò di cui hanno bisogno. È importante notare che questo servizio è posseduto e mantenuto dai team dei consumatori e non dai team che possiedono i servizi di dominio. I team di front-end possono ora ottimizzare in base alle loro esigenze – un’app mobile può richiedere un payload più piccolo, ridurre il numero di chiamate dall’app mobile, e così via. Dai un’occhiata alla vista rivista dell’orchestrazione qui sotto. Il servizio BFF ora chiama entrambi i servizi di dominio Ordini e Rimborsi per il suo caso d’uso.

Fig 9. Backend per Frontend

È anche utile costruire il servizio BFF all’inizio, prima di rompere una pletora di servizi dal monolite. Altrimenti, o i servizi del dominio dovranno supportare l’orchestrazione interdominio, o le applicazioni web e mobili dovranno chiamare più servizi direttamente dal front-end. Entrambe queste opzioni porteranno a un sovraccarico di prestazioni, lavoro di scarto e mancanza di autonomia tra i team.

Conclusione

In questo blog, abbiamo toccato vari concetti, strategie ed euristiche di progettazione da considerare quando ci avventuriamo nel mondo dei microservizi, più specificamente quando cerchiamo di rompere un monolite in più microservizi basati sul dominio. Molti di questi sono argomenti vasti da soli, e non credo che abbiamo fatto abbastanza giustizia per spiegarli in pieno dettaglio, ma abbiamo voluto introdurre alcuni degli argomenti critici e la nostra esperienza nell’adottarli. La sezione Further Reading (link) ha alcuni riferimenti e alcuni contenuti utili per chiunque voglia perseguire questo percorso.

Aggiornamento: I prossimi due blog della serie sono usciti. Questi due blog discutono l’implementazione del microservizio Cart, con esempi di codice, usando i principi del Domain-Driven Design e i design pattern Ports and Adapters. L’obiettivo principale di questi blog è dimostrare come questi due principi/modelli ci aiutano a costruire applicazioni modulari che sono agili, testabili e rifattorizzabili – in breve, essere in grado di rispondere all’ambiente frenetico in cui tutti operiamo.

Implementazione del microservizio Cart usando il Domain Driven Design e lo schema di porte e adattatori – Parte 1

Implementazione del microservizio Cart usando il Domain Driven Design e lo schema di porte e adattatori – Parte 2

Altre letture

1. Domain Driven Design di Eric Evans

2. Implementazione del Domain Driven Design di Vaughn Vernon

3. Articolo di Martin Fowler sui microservizi

4. Costruire microservizi di Sam Newman

5. Event storming

7. Backend per Frontend

8. Fallacie del calcolo distribuito

.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.