Arquitectura
Arquitectura i fronteres
Definició de frontera
L’arquitectura d’un sistema defineix com es divideix en components, quines són les fronteres o límits (boundaries) d’aquests components i com es comuniquen a través d’aquestes. Quan no hi ha fronteres parlem del monòlit.
El propòsit d’aquestes fronteres és facilitar el seu desenvolupament, gestió, manteniment i evolució. Una bona definició de les fronteres redueix l’acoblament, facilitant la flexibilitat de modificar unes parts sense afectar unes altres, i evita la degradació gradual de l’arquitectura. D’això se’n diu arquitectura evolutiva.
- Facilita el desenvolupament, perquè permet desenvolupar les parts de forma independent, a ritmes diferents.
- Facilita les proves, podem crear objectes que simulen comportament (test doubles), com dummies, stubs, mocks o fakes.
- Facilita els canvis i l’evolució, ja que podem canviar la implementació d’un comportament sense afectar la resta.
En el fons, definir fronteres és aplicar la separació de responsabilitats (separation of concerns, SoC): agrupar allò que canvia junt i separar allò que canvia per motius diferents. Si ho hem fet bé es reflecteix en dues propietats complementàries:
- L’acoblament és el grau de dependència entre components. El volem baix, perquè així podem modificar-ne un sense arrossegar els altres.
- La cohesió és el grau en què els elements d’un mateix component pertanyen realment junts. La volem alta, perquè un component centrat en una sola responsabilitat és més fàcil d’entendre, provar i reutilitzar.
Aquests dos objectius, baix acoblament i alta cohesió, són el criteri de fons per decidir on posar una frontera.
Les dependències entre components haurien de ser un graf acíclic dirigit. Això evita les dependències circulars, que augmenten l’acoblament dels components i limiten la possibilitat de reutilitzar-los de forma individual.
Tipologia de fronteres
Tipus de separació
Existeixen dos enfocaments principals per crear fronteres en sistemes de programari: la separació horitzontal i la separació vertical.
- L’horitzontal crea les fronteres entre àrees tècniques del sistema. Per exemple, una API, la lògica de negoci i la comunicació amb la base de dades. Els canvis impliquen habitualment diferents capes del sistema. Pot ser un problema si les capes les gestionen diferents equips de desenvolupament.
- La vertical crea la frontera entre àrees funcionals del sistema. S’utilitza amb microserveis. Per exemple, la gestió d’usuaris o la creació de comandes. Els canvis en aquest tipus de separació són més àgils.
Mecanismes de separació
Les fronteres es poden implementar mitjançant quatre mecanismes diferents:
- Codi font: utilitzant classes i interfícies per poder comunicar-se mitjançant mètodes sense veure la implementació. Si es fa bé (actuant amb bona fe), permet aïllar les parts per permetre múltiples equips treballant. És l’únic mecanisme dels monòlits.
- Components vinculats dinàmicament: es fa una separació amb components desplegables, per exemple, arxius JAR. Es comuniquen amb crides a mètodes, i poden utilitzar el principi d’inversió de dependència per a establir les relacions entre ells.
- Processos locals: tenim processos locals que estan a la mateixa màquina. Poden comunicar-se utilitzant memòria compartida o sòcols. Permeten utilitzar diferents entorns de desenvolupament i tecnologies, sempre que es comparteixi el protocol.
- Serveis: permet que els serveis estiguin a diferents màquines i utilitzin la xarxa. No se sol compartir la base de dades (mala pràctica). S’estableixen protocols estàndards basats en la xarxa, com REST. Exemple dels microserveis.
Resumint, els mecanismes de separació es poden classificar segons el seu enfocament:
- Basats en interfícies: Codi font i components vinculats dinàmicament.
- Basats en protocols: Processos locals i serveis.
Estils d’arquitectura
L’arquitectura d’una solució pot ser monolítica o distribuïda. La distribuïda sol basar-se en client-servidor, tot i que també tenim la solució peer-to-peer.
A continuació revisarem alguns dels estils d’arquitectura.
- Multicapa (multitier). Basada en la client-servidor, estableix una sèrie de capes independents que permeten desenvolupar cada capa de forma prou independent. Les capes habituals són:
- Presentació
- Aplicació
- Dades
- Microkernel. Es tracta d’una arquitectura amb una funcionalitat central que permet afegir funcionalitats addicionals en forma de plug-ins.
- Orientada a esdeveniments (event-driven). Basada en la comunicació asíncrona d’esdeveniments. Associada al concepte de “event-broker”, un middleware que s’encarrega d’encaminar esdeveniments entre sistemes que implementen el patró productor-consumidor. Els missatges poden ser guardats a cues fins a ser processats.
- Microserveis. És l’ús paradigmàtic de la frontera de servei que hem vist als mecanismes de separació: un ecosistema de serveis d’un propòsit senzill que es comuniquen mitjançant un gateway d’APIs. Cada servei utilitza les seves pròpies dades.
Patrons d’arquitectura
L’arquitectura hexagonal és un model arquitectural que permet separar el core de negoci (o domini) de la infraestructura (UI, base de dades, APIs, frameworks, etc.). Ho fa proposant els adaptadors i els ports.
Els adaptadors fan la interacció de la nostra aplicació cap al món. Tenim dos tipus:
- Els primaris: punts d’entrada de l’aplicació, operats principalment pels usuaris (UI, API Rest, CLIs).
- Els secundaris: actors secundaris com les bases de dades o serveis de tercers.
Si volem seguir el principi d’inversió de dependència, les capes internes no poden dependre de les externes. O sigui, les dependències han de ser de fora a dins: dels adaptadors cap al core.
Els ports són fronteres abstractes a l’exterior, per exemple, utilitzant interfícies, i els adaptadors són implementacions concretes dels ports, habitualment injectades:
- Els adaptadors primaris depenen de ports d’entrada, i dirigeixen l’aplicació (API, GUI, CLI).
- Els adaptadors secundaris implementen ports de sortida, i són dirigits per l’aplicació (BBDD, API clients).
DDD (Domain-driven design) és una estratègia de disseny de software focalitzat en el modelatge del software, i que té l’objectiu de replicar una àrea temàtica o domini (domain) gràcies als coneixements dels experts d’aquesta àrea.
Dins del DDD hi trobem diversos tipus de models, com les entitats, que tenen identitat, o els objectes de valor (value objects), immutables i sense identitat. També hi ha aggregates, que són altres models dirigits per una entitat arrel, i que són unitats de consistència, concurrència i distribució.
De vegades hem de comunicar el nostre domini amb altres externs, que provoquen una dependència externa. És una mala estratègia permetre que es filtri aquest model extern cap al nostre, i la forma d’evitar-ho és construir una capa anti-corrupció. La idea tècnica es pot implementar utilitzant els patrons Facade i Adapter.
L’arquitectura clean afegeix el concepte d’entitats i casos d’ús al core. Tant l’arquitectura hexagonal com la clean es recolzen en el DDD. Les capes que defineix, de més interna a més externa són:
- Les entitats: objectes de negoci amb comportament
- Els casos d’ús (use cases): mapejat de la funcionalitat de les user stories
- Els adaptadors d’interfícies (controladors, vistes, presentadors)
- Els frameworks i eines (BBDD, serveis de tercers)
La regla de dependència és la mateixa idea que acabem de veure a l’arquitectura hexagonal, formulada per a aquestes capes: cap capa interna ha de dependre d’una externa. Això es manifesta de la següent forma:
- Sense dependències:
- Les entitats no depenen de ningú.
- Dependències cap a dins:
- Els casos d’ús depenen de les entitats, la capa més interna.
- Els adaptadors d’entrada depenen dels casos d’ús.
- Dependències cap a fora, però abstractes (inversió de dependència):
- Quan un cas d’ús necessita alguna cosa de l’exterior, per exemple la persistència, depèn d’un port de sortida que ell mateix defineix, i és l’adaptador qui l’implementa. Així la dependència continua apuntant cap a dins.

El diagrama mostra una arquitectura hexagonal amb conceptes DDD (clean). Les fletxes negres indiquen dependència d’una interfície i les blanques, implementació.
Cal no confondre la direcció de les dependències amb el flux de control. Per exemple, un flux de control d’una aplicació amb el patró MVP (model-vista-presentador) podria ser controlador => cas d'ús => entitats => cas d'ús => presentador.
Finalment, cal dir que el cablejat dels components de l’arquitectura es fa des de fora de l’aplicació, creant les dependències, connectant-les i iniciant l’aplicació. El patró es diu composition root. Aquí sol utilitzar-se la injecció de constructor i, si hi ha contenidors que fan inversió de control, és l’únic lloc on haurien d’aparèixer.
Disseny per contracte
Hem vist quines fronteres hi ha i on solen col·locar-se. Ara baixem un nivell: com s’especifica una frontera concreta. El disseny per contracte és una forma de dissenyar formalment les interfícies dels components d’un software respecte de qui els crida, o clients. Aquest contracte té dues parts:
- Els requisits que demana el component als clients.
- Les promeses fetes pel component als clients.
Resumint, si el client compleix els requisits, el component promet complir el contracte que defineix.
Si canviem un contracte, volem que els clients no quedin afectats. Per assegurar-nos utilitzem la frase “no requerir més ni prometre menys”: si el canvi no requereix més dels clients ni promet menys, la nova especificació és compatible i no trencarà el funcionament del client.
Algunes pràctiques per a seguir el disseny per contracte:
-
Documentar el contracte amb comentaris, responent quins són els requisits i les promeses que es fan. En resum, explicant exactament què es fa sense haver de saber com. A Java es fa al javadoc de la classe i dels mètodes públics.
-
Validar arguments dels mètodes i constructors públics. És raonable no fer-ho amb els privats, ja que només es criden per la mateixa classe. D’això també se’n diu precondicions. A Java, aquesta validació sol produir excepcions unchecked com IllegalArgumentException, NullPointerException o IllegalStateException que no cal obligatòriament documentar.
-
Validar les promeses que fa un mètode al seu client. D’això també se’n diu postcondicions. Per exemple, comprovar valors, tipus de retorn, errors i excepcions que es produeixen. Es pot fer just abans d’acabar un mètode públic.
-
Validar l’estat de l’objecte. D’això també se’n diu invariants de classe. Implica mantenir una sèrie de condicions sobre l’estat entre les crides a mètodes públics. Si l’objecte és immutable, només cal validar l’estat al constructor. Si és mutable, a cada mètode que canvia l’estat.
-
Opcionalment, aspectes sobre el rendiment (temps i espai).
Estratègia de gestió d’errors
El disseny per contracte defineix què promet un component i què exigeix. L’estratègia de gestió d’errors defineix què passa quan aquestes promeses no es poden complir. És una decisió de disseny que afecta tota l’arquitectura i que convé prendre explícitament: si no es fa, cada part del sistema gestionarà els errors de manera diferent.
Fail-fast vs. defensiu
Hi ha dos enfocaments fonamentals:
- Fail-fast: quan es detecta un estat invàlid, el sistema falla immediatament i de forma visible. Això facilita el debugging perquè l’error es manifesta a prop de la causa. Les precondicions del disseny per contracte segueixen aquest principi: si un argument és invàlid, es llança una excepció al moment.
- Defensiu: el sistema intenta continuar operant malgrat condicions inesperades, amb valors per defecte, reintents o degradació de funcionalitat. Adequat per a sistemes que han de mantenir disponibilitat (p. ex. un frontend que mostra dades parcials en lloc de fallar completament).
La majoria de sistemes combinen els dos: fail-fast a la lògica interna (on un error és un bug) i defensiu a les fronteres (on les entrades són impredictibles).
Tipus d’errors
No tots els errors tenen el mateix tractament. Una classificació útil:
- Errors de programació (bugs): precondicions violades, índexs fora de rang, nulls inesperats. No s’han de gestionar, s’han de corregir. Fail-fast amb una excepció no recuperable.
- Errors de domini: regles de negoci que no es compleixen (saldo insuficient, usuari no autoritzat). Formen part del contracte del component i s’han de comunicar explícitament al client, amb excepcions de domini, tipus de retorn que representin el fracàs, o codis d’error documentats.
- Errors d’infraestructura: xarxa no disponible, disc ple, servei extern caigut. Són temporals i sovint recuperables amb reintents. S’han de gestionar a les fronteres del sistema (adaptadors), no al cor de la lògica.
On capturar els errors
Un error s’hauria de capturar al lloc on es pot prendre una decisió útil sobre què fer. Capturar-lo abans produeix codi que emmascarà problemes; capturar-lo després complica la depuració.
- A les fronteres d’entrada: controladors, handlers d’API, punts d’entrada de CLI. Aquí es transformen errors interns en respostes adequades pel client (codis HTTP, missatges d’error).
- A les fronteres de sortida: adaptadors de base de dades, clients HTTP, sistemes de fitxers. Aquí es decideix si reintentar, degradar o propagar.
- No al mig: la lògica de negoci hauria de propagar errors, no capturar-los. Un
try-catchgenèric dins de la lògica de domini sol ser un senyal que falta una frontera ben definida.
Diagrames d’arquitectura
El model C4 és un model que permet visualitzar l’arquitectura d’una solució. Descriu quatre nivells de diagrames, de més generals a més concrets:
- De context de sistema: mostra el sistema software i el seu context al voltant.
- De contenidor: mostra els contenidors dins d’un sistema software i com es relacionen.
- De component: mostra els components d’un contenidor i les seves interaccions.
- De codi: mostra la implementació del codi amb diagrames UML, diagrames ER o similars.
Hi ha quatre abstraccions que poden aparèixer en el nostre diagrama:
- La persona: actors o rols d’un sistema software.
- El sistema software: el sistema que estàs modelant, i que es relaciona amb altres sistemes externs.
- Un contenidor: una aplicació o un magatzem de dades. Per exemple:
- Un backend web
- Un frontend web
- Una aplicació desktop (client)
- Una app mòbil
- Una aplicació de consola o script
- Una funció serverless (cloud)
- Una base de dades
- Un magatzem al núvol
- El sistema d’arxius
- Un component: grup de funcionalitats encapsulat darrere d’una interfície (o contracte). Tots els components d’un contenidor solen executar-se en el mateix espai de processos o màquina virtual.
Alguns consells per a dibuixar diagrames:
- No hi ha una forma estàndard de fer-ho.
- Han de reflectir la realitat. Abstraccions primer, notació després.
- Conté blocs per als diferents components (SoC). Alguna cosa modular amb una interfície/fronteres. Els components contenen codi.
- Agrupar els components que treballen junts amb caixes (contenidors): DB schema, app mòbil, backend server-side app, console app, windows service.
- Ha d’incloure les dades, la lògica de negoci i la interfície d’usuari.
- Fletxes sempre direccionals per indicar la direcció del flux (petició) amb descripció.
- No cal afegir respostes (fletxes de tornada) dient OK, només qui les origina.
- Ser consistent en forma i color, evitar acrònims no coneguts.
Estils de frontera de servei
La frontera de servei (el mecanisme de separació més comú en sistemes distribuïts) es pot comunicar de tres maneres fonamentals, distingides pel model d’interacció, no per qui hi ha a cada costat (tots tres estils apareixen tant de client a servei com de servei a servei):
- Síncrona (petició/resposta): el client envia una petició i espera la resposta. És el model de REST sobre HTTP. Senzill de raonar i de depurar, però acobla el client i el servei en el temps: si el servei no respon, el client queda bloquejat o falla.
- Asíncrona (missatgeria i esdeveniments): el productor diposita un missatge en un intermediari i continua sense esperar; un consumidor el processa més tard. Desacobla els dos costats en el temps, a canvi de més complexitat i consistència eventual.
- Streaming (connexió persistent): client i servei mantenen una connexió oberta per la qual les dades flueixen a mesura que es produeixen. Ni petició puntual ni intermediari: un canal continu i en temps real.
La majoria de sistemes reals combinen aquests estils segons el cas. Els microserveis en són l’exemple paradigmàtic: cada servei exposa aquesta frontera i s’hi comunica amb la resta, sovint amb REST per a les consultes síncrones i amb missatgeria per als esdeveniments, tot passant per un gateway d’APIs com hem vist als estils d’arquitectura.
Síncron: petició/resposta
REST (Representational State Transfer) és l’estil de petició/resposta més estès, habitualment sobre HTTP amb cossos en format JSON, i és el que veurem en detall. Dins d’aquest mateix model, però, en la pràctica també trobaràs:
- gRPC: crides a procediment remot (RPC) sobre HTTP/2, amb missatges binaris definits amb Protocol Buffers i un contracte fortament tipat. Eficient i molt usat en la comunicació interna entre microserveis; també admet streaming.
- GraphQL: un únic endpoint on el client demana exactament les dades que necessita, i evita obtenir-ne de més o de menys (over-fetching i under-fetching). Habitual en APIs orientades a clients amb necessitats de dades diverses.
- SOAP i XML-RPC: estils RPC més antics basats en XML, que encara apareixen en sistemes empresarials i integracions heretades.
En la pràctica:
- Protocol de xarxa: HTTP sobre TCP. REST i SOAP solen anar sobre HTTP/1.1; gRPC exigeix HTTP/2, que multiplexa moltes crides en una sola connexió.
- Ús des de la solució: fas una crida amb un client HTTP (per exemple
fetch,axios,HttpClientoWebClient) i esperes l’objecte de resposta; sovint amb un client o SDK generat a partir del contracte (obligatori a gRPC, a partir del fitxer.proto). - Quan usar-lo: quan el client necessita la resposta immediata i la interacció és clarament una consulta o una ordre amb resultat: CRUD sobre recursos, lectures o validacions.
Criteris de disseny
- Identificar els recursos que formen part de l’API i els seus identificadors.
- Definir l’URI de cada recurs, o endpoint, amb noms i mai amb verbs. Si cal executar una acció, expressar-la sobre un nom, per exemple amb un paràmetre
actiona la query. - Anomenar en plural els endpoints de col·leccions.
- Retornar la informació completa quan es demana un sol recurs, i reduir-la a l’estrictament necessària quan es retornen col·leccions.
- Assignar a cada endpoint els mètodes HTTP adequats, amb els codis d’estat corresponents.
- Utilitzar JSON com a format del cos, tant per enviar com per rebre dades.
- Aplicar filtres, ordenació i paginació a les col·leccions.
- Generar documentació de l’API amb OpenAPI.
Mètodes
El content type a utilitzar amb les peticions és “application/json”. Usos dels mètodes HTTP:
- GET recupera una representació del recurs a l’URI especificat.
- Si es troba, 200 (Ok). El cos del missatge de resposta conté els detalls del recurs sol·licitat.
- Si no es troba, 404 (Not Found).
- També es pot retornar 204 (No Content) si ha anat bé, però no es retorna cap contingut.
- Si les dades enviades no són vàlides, 400 (Bad Request). El cos pot incloure informació addicional sobre el problema.
- POST crea un recurs nou a l’URI especificat. El cos del missatge de sol·licitud proporciona els detalls del nou recurs. Cal tenir en compte que POST també es pot utilitzar per activar operacions que en realitat no creen recursos.
- Si es crea un nou recurs, 201 (Created). El recurs pot retornar-se al cos.
- Si es fa algun procés, però no es crea res, 200 (Ok). El cos pot incloure el resultat de l’operació. Alternativament, si no hi ha resultat, es pot retornar 204 (No Content) sense cos.
- Si les dades enviades no són vàlides, 400 (Bad Request). El cos pot incloure informació addicional sobre el problema.
- PUT crea o substitueix el recurs a l’URI especificat. El cos del missatge de sol·licitud especifica el recurs que s’ha de crear o actualitzar.
- Si es crea un nou recurs, 201 (Created).
- Si s’actualitza, 200 (Ok) o 204 (No Content).
- Si no és possible l’actualització, 409 (Conflict).
- PATCH realitza una actualització parcial d’un recurs. El cos de la sol·licitud especifica el conjunt de canvis que cal aplicar al recurs. Les respostes podrien ser com les de PUT.
- DELETE elimina el recurs a l’URI especificat.
- Si funciona, 204 (No Content), sense retornar cap informació.
- Si es retorna alguna informació també es pot utilitzar 200 (Ok).
- Si no existeix, 404 (Not Found).
Aquests són alguns exemples d’endpoints i com se solen utilitzar segons els mètodes:
| Recurs | POST | GET | PUT | DELETE |
|---|---|---|---|---|
| /customers | Crear un nou client | Obtenir tots els clients | Actualitzar tots els clients | Esborrar tots els clients |
| /customers/1 | N/A | Obtenir els detalls del client 1 | Actualitzar els detalls del client 1, si existeix | Esborrar client 1 |
| /customers/1/orders | Crear una nova comanda per al client 1 | Obtenir totes les comandes del client 1 | Actualitzar totes les comandes del client 1 | Esborrar totes les comandes del client 1 |
Filtres, ordenació i paginació
Quan s’exposa un conjunt de recursos a un endpoint, cal evitar retornar una quantitat molt gran de dades. L’API hauria de permetre especificar filtres a l’URI: recursos?filtre1=valor1&filtre2=valor2…. També caldria especificar a l’URI com obtenir només una part dels resultats, quan poden ser molts.
L’ordenació també es pot realitzar amb un paràmetre del tipus recursos?order_by=criteri.
Cal utilitzar paginació sempre que una col·lecció de recursos pugui ser gran perquè pugui créixer sense límit. Es pot fer principalment de tres formes:
- offset:
recursos?limit=nombre&offset=nombrepermet utilitzar els paràmetres SQL per limitar els resultats. L’opció més senzilla, però poc òptima per a offsets alts: cal obtenir tots els registres anteriors en la query. - keyset: filtra pel valor d’un camp que defineix l’ordre. Per exemple, la data de creació. Podem utilitzar
recursos?limit=nombre&from_date=data. - seek: similar a l’anterior, però utilitzant una primary key. Podem utilitzar
recursos?limit=nombre&after_id=id.
Per a la paginació, pots utilitzar un valor de límit per defecte si no es diu res. Per exemple, 20. No s’hauria de permetre un valor qualsevol per a aquest paràmetre (ha d’estar limitat). També pots retornar informació al cos de la resposta que pugui ajudar al client a gestionar el resultat.
Asíncron: missatgeria i esdeveniments
En comptes d’una conversa directa, el productor diposita missatges en un intermediari (broker) que els encamina cap als consumidors. És l’estil de l’arquitectura orientada a esdeveniments que hem vist als estils d’arquitectura. Hi ha tres patrons bàsics:
- Cues (point-to-point): cada missatge el processa un únic consumidor. Serveix per repartir feina i posar-la a la cua quan hi ha pics de càrrega.
- Publicació/subscripció: un esdeveniment es difon a tots els subscriptors interessats. El productor no sap qui l’escolta, cosa que desacobla fortament els sistemes.
- Registre d’esdeveniments (log): els esdeveniments s’afegeixen a un registre append-only (només s’hi afegeix, mai es modifica) i s’hi conserven; diversos consumidors el llegeixen al seu ritme i en poden rellegir l’historial. És la base de l’streaming d’esdeveniments d’alt volum.
En la pràctica:
- Protocol de xarxa: els protocols de missatgeria van sobre TCP i són oberts, de manera que productors i consumidors de tecnologies diferents es comuniquen sense acoblar-se a una implementació. Els més habituals són AMQP (Advanced Message Queuing Protocol, cues fiables), MQTT (lleuger, pub/sub, molt usat en IoT) i STOMP (senzill, basat en text).
- Ús des de la solució: amb la llibreria client del broker, el productor publica un missatge en una cua o tema i continua; el consumidor registra un listener (una funció de callback) que s’executa quan arriba cada missatge, sovint en un procés treballador en segon pla.
- Quan usar-lo: quan vulguis desacoblar productors i consumidors, processar en segon pla, anivellar pics de càrrega o difondre esdeveniments a diversos sistemes, i no calgui una resposta immediata.
Els compromisos respecte del model síncron són:
- A favor: desacoblament en el temps (el consumidor pot estar caigut i processar més tard), resiliència, absorció de pics de càrrega i escalabilitat independent de productor i consumidor.
- En contra: més peces mòbils (el broker), consistència eventual, i cal gestionar l’ordre i els missatges duplicats. Les garanties de lliurament habituals són “com a mínim un cop” (at-least-once), “com a molt un cop” (at-most-once) i “exactament un cop” (exactly-once), aquesta última difícil i costosa.
- El flux és més difícil de seguir, perquè no hi ha una traça de petició/resposta única.
Una variant sense broker molt estesa són els webhooks: en lloc de consultar repetidament un servei per saber si ha passat res (polling), hi registres una URL i el servei t’hi fa una petició HTTP (un callback) quan es produeix l’esdeveniment. És el mecanisme habitual per rebre notificacions de serveis externs, com passarel·les de pagament o repositoris de codi.
Streaming: connexió persistent
En comptes de peticions puntuals o de missatges a través d’un intermediari, els dos extrems obren una connexió persistent per la qual el servei empeny dades a mesura que es produeixen. Els dos protocols habituals són:
- Server-Sent Events (SSE): unidireccional, només del servidor cap al client, amb reconnexió automàtica.
- WebSockets: bidireccional i full-duplex.
En la pràctica:
- Protocol de xarxa: tots dos comencen amb un handshake HTTP i després mantenen una connexió TCP oberta. SSE continua sent HTTP (un flux
text/event-stream); WebSockets fa un canvi (upgrade) a un protocol propi de trames sobre la mateixa connexió TCP. - Ús des de la solució: al navegador, SSE s’usa amb l’objecte
EventSourcei WebSockets amb l’APIWebSocket(o llibreries com Socket.IO); en tots dos casos registres callbacks (onmessage,onopen) per reaccionar a les dades que arriben. Al servidor mantens el handler que empeny esdeveniments mentre la connexió és viva. - Quan usar-lo: quan calen actualitzacions contínues en temps real i baixa latència (notificacions en viu, xats, taulers, edició col·laborativa). Tria SSE si el flux va només del servidor al client; WebSockets si els dos costats han d’enviar dades.
A diferència de la missatgeria, aquí no hi ha broker que desacobli els extrems ni que emmagatzemi els missatges: si la connexió es talla, cal restablir-la (SSE ho fa sol; amb WebSockets ho gestiona l’aplicació). És el model amb menys latència, a canvi de mantenir connexions obertes, cosa que afegeix pressió sobre els recursos del servidor.
Abans que SSE i WebSockets fossin habituals, el mateix efecte de temps real s’aconseguia amb el long polling (el client fa una petició que el servidor manté oberta fins que té dades noves, i llavors en fa una altra). Encara es troba com a alternativa de compatibilitat quan SSE o WebSockets no estan disponibles.
Arquitectura en equip
Fins ara hem tractat l’arquitectura com una decisió tècnica. Però una arquitectura només existeix mentre l’equip la manté viva: les fronteres que hem definit es dilueixen tan bon punt algú, amb pressa, hi fa una drecera. Ja ho hem vist amb el mecanisme de separació per codi font, que només funciona actuant amb bona fe. El repte, doncs, és sociotècnic, i la part difícil no és el codi.
Hi ha dues maneres típiques de fallar quan algú intenta posar ordre en l’arquitectura d’un equip, i totes dues apareixen també en projectes petits:
- Decidir-ho tot al principi: es dissenya l’arquitectura els primers dies, es documenta i ningú no torna a obrir el document. Al cap d’un mes, el codi i el document ja no s’assemblen.
- Revisar-ho tot: una persona revisa i reescriu el codi de la resta. O bé es converteix en un coll d’ampolla que frena l’equip, o bé la resta acaba treballant al marge d’ella.
El problema de fons és que no es pot encertar el disseny des del principi. Les fronteres bones s’aprenen construint, i per això la iteració ràpida sol guanyar al disseny previ detallat. L’objectiu, per tant, no és encertar l’arquitectura, sinó evitar que es degradi mentre canvia. És el que hem anomenat arquitectura evolutiva, i s’aconsegueix amb tres mecanismes: la cultura de l’equip, les decisions explícites i les comprovacions automàtiques.
Cultura: propietat i raonament
Dues pràctiques barates i amb molt d’efecte:
- Qui ho construeix, ho manté (you build it, you run it). Qui escriu un component és qui l’arregla quan falla i qui l’explica a la resta. L’alternativa, llançar el codi per damunt del mur perquè el problema passi a ser d’algú altre, garanteix que ningú no assumeixi les conseqüències de les seves pròpies decisions.
- Explicar el perquè, no només el què. Quan revisem el codi d’algú, dedicar una mica més de temps a explicar el raonament d’un canvi fa que la propera vegada no calgui dir-ho. La revisió de codi és sobretot una eina de transmissió: el criteri es propaga i s’acaba aplicant sense que hi siguem al davant. Ajuda tenir un petit vocabulari compartit d’exemples i comparacions que l’equip repeteix.
Decisions explícites: els ADR
Un document de visió llarg envelleix malament i ningú no el llegeix. L’alternativa habitual són els ADR (Architecture Decision Records): un fitxer curt per decisió, guardat al repositori i versionat amb el codi, amb quatre apartats.
- Context: quin problema teníem i amb quines restriccions.
- Decisió: què hem decidit fer.
- Estat: proposada, acceptada o substituïda per un altre ADR.
- Conseqüències: què hi guanyem i què hi perdem, incloent-hi què ens costarà si algun dia ho volem canviar.
El valor no és el document en si, sinó deixar constància de per què les coses són com són. Quan algú es planteja saltar-se una frontera, l’ADR li diu si està trencant una decisió pensada o simplement una casualitat històrica. I quan una decisió deixa de tenir sentit, se substitueix per una de nova en lloc d’editar la vella: l’historial de decisions també és informació.
Comprovacions automàtiques: funcions d’aptitud
Les regles que depenen de la bona voluntat es trenquen tard o d’hora. Les que es comproven soles, no. A les comprovacions executables que verifiquen que el sistema manté les seves propietats arquitectòniques se’ls diu funcions d’aptitud (fitness functions), i són el mecanisme central de l’arquitectura evolutiva:
- Proves: no verifiquen l’arquitectura directament, però la dificultat d’escriure-les sí que informa. Si per muntar una prova unitària necessites molts test doubles, el component té massa dependències; si necessites una base de dades per comprovar una regla de negoci, la lògica i la infraestructura estan barrejades.
- Tipatge fort: detecta errors sistèmics que les proves no cobreixen, com el canvi de la signatura d’un mètode que afecta molts punts alhora.
- Regles de dependència automatitzades: amb eines com ArchUnit (Java) es pot escriure com una prova més la regla “el domini no pot dependre de la infraestructura”. La regla de dependència de l’arquitectura clean deixa de ser una recomanació i passa a ser una prova que falla.
- Format i estil automatitzats: els linters i els formatadors treuen de la revisió tota la discussió que no aporta res i deixen espai per parlar del que importa.
- Observabilitat: registres, mètriques i traces. Sense veure què passa en execució, els casos límit només apareixen quan ja són un problema, i el cicle de correcció s’allarga.
Totes s’han d’executar a la integració contínua, no a la màquina de qui se’n recorda: una regla que no bloqueja la integració és només una recomanació. Comença a ser habitual complementar-les amb revisors automàtics basats en models de llenguatge, capaços de detectar regles més difuses (lògica de negoci que s’escola cap a la capa d’API, duplicacions), tot i que de moment no substitueixen les comprovacions deterministes.
Llegir els senyals de les proves
De tots aquests mecanismes, les proves són l’únic que qualsevol equip ja té, i val la pena saber-ne llegir els senyals. La taula relaciona la dificultat que trobes en escriure una prova amb el problema de disseny que sol indicar:
| Símptoma en escriure la prova | Què sol indicar |
|---|---|
| Necessites molts test doubles només per instanciar la classe | Massa dependències i poca cohesió: la classe fa més d’una cosa |
| Necessites una base de dades, la xarxa o el sistema de fitxers per comprovar una regla de negoci | El domini i la infraestructura estan barrejats: falta un port |
| La preparació (arrange) és més llarga que la resta de la prova | Falta una abstracció, o l’invariant hauria de viure en un altre objecte |
| Has d’ampliar la visibilitat d’un mètode o fer servir reflexió per comprovar res | Estàs provant la implementació i no la promesa: el contracte públic no és el correcte |
| Canvis no relacionats trenquen la prova contínuament | La frontera no està al lloc adequat i el contracte deixa escapar detalls d’implementació |
| No saps què comprovar | El contracte no està definit |
L’últim cas és el més útil de tots, perquè enllaça amb el disseny per contracte: una classe amb un contracte ben definit és fàcil de provar, ja que la prova són les precondicions i les postcondicions. Si no saps què afirmar, el problema és anterior a la prova.
Compte, però, que no tota dificultat és un problema de disseny. La lògica de domini complexa, la concurrència i el codi que depèn del temps són difícils de provar per motius legítims, i les proves d’integració i d’extrem a extrem són cares de preparar per definició. El senyal només val per a les proves unitàries, que és on controlem la frontera.
La llei de Conway
La llei de Conway diu que l’estructura d’un sistema tendeix a reproduir l’estructura de comunicació de l’organització que el construeix. Ho hem insinuat en parlar de la separació horitzontal: si cada capa la manté una persona diferent, qualsevol funcionalitat nova necessita coordinar-les totes i acaba sent lenta. Si, en canvi, cada persona s’encarrega d’una àrea funcional completa, com en la separació vertical, els canvis es poden fer de punta a punta.
La conseqüència pràctica és que, quan es reparteix la feina d’un equip, s’està decidint també quina arquitectura acabarà sortint. Val la pena fer-ho a consciència: primer decidim les fronteres i després repartim la feina d’acord amb elles, no a l’inrevés.
A la pràctica
En un projecte d’equip petit, tot això es tradueix en poques coses concretes:
- Un directori
docs/adr/amb les decisions que us han costat una discussió. - Una prova de dependències que falli si algú importa la infraestructura des del domini.
- Un linter i un formatador executats a la integració contínua.
- Un responsable identificable per a cada component, i revisions de codi on s’expliqui el perquè.
En resum, la feina d’arquitectura d’un equip no és vigilar el codi, sinó cuidar el sistema que el produeix (garden the system, not the codebase): les persones, els acords i les comprovacions automàtiques. L’estructura dels components n’és el resultat.
Referències
- The C4 model for visualising software architecture
- PlantUML
- UML diagrams with plantUML
- Software Architecture Boundaries
- Acyclic Dependencies Principle
- Design by Contract
- Hexagonal architecture
- The Clean Architecture
- Hexagonal Architecture with Java and Spring
- Domain-centric Architectures (Clean and Hexagonal) for Dummies
- Architectural boundaries
- How to be useful as a software architect
- Architecture Decision Records
- Documenting Architecture Decisions
- Fitness function-driven development
- ArchUnit
- Conway’s law
- RESTful web API design
- REST API Design: Filtering, Sorting, and Pagination
- What is REST
- OpenAPI