Tutti gli articoli su REST vs gRPC si assomigliano: benchmark con numeri impressionanti, diagrammi puliti, conclusione diplomatica tipo "dipende dal caso d'uso".
Questo articolo è diverso. Ti racconto un progetto enterprise reale dove ho adottato gRPC, misurato il costo reale sul campo con oltre 50 entità, e poi l'ho rimosso. E ti spiego l'evoluzione architettuale finale che ha risolto davvero il problema.
Architettura a microservizi con un DAL centralizzato
Il progetto era un sistema enterprise a microservizi in ambiente Spring Boot. L'architettura iniziale prevedeva un DAL (Data Access Layer) separato come microservizio autonomo, con il compito di isolare completamente la logica di persistenza dal resto dei servizi.
L'idea sulla carta era solida:
- I microservizi non toccano mai il database direttamente
- Tutta la logica JPA/Hibernate vive in un posto solo
- I MS comunicano col DAL tramite un'interfaccia di rete
Stack del DAL: Spring Data JPA + Hibernate puro.
Protocollo scelto inizialmente per la comunicazione: gRPC.
Fase 1 — gRPC tra MS e DAL: il problema del triple mapping
gRPC sembrava la scelta giusta. Contratto forte, performance, streaming. I tutorial lo vendono bene.
Il problema emerge non sul primo servizio, non sul decimo — ma quando il sistema cresce e ti ritrovi con oltre 50 entità.
Il flusso di trasformazione reale
Per ogni operazione CRUD, il dato doveva attraversare tre rappresentazioni diverse:
[Microservizio — lato chiamante]
│
│ ① Java DTO (oggetto di dominio del MS)
▼
Mapper DTO → Protobuf
│
│ ② Protobuf Message (wire format binario su rete)
▼
[DAL Microservizio — lato server gRPC]
│
│ ③ Protobuf Message ricevuto
▼
Mapper Protobuf → JPA Entity
│
│ ④ JPA Entity (oggetto Hibernate)
▼
Spring Data JPA → Database
Quattro rappresentazioni dello stesso dato. Due layer di mapping obbligatori per ogni singola entità, in entrambe le direzioni.
Il costo reale con 50+ entità
Prendiamo un'entità semplice come esempio:
// user.proto
syntax = "proto3";
message User {
int64 id = 1;
string username = 2;
string email = 3;
string created_at = 4; // proto3: no LocalDateTime nativo
}
// JPA Entity
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private LocalDateTime createdAt; // qui già diverge dal proto
}
// DTO lato microservizio
public class UserDTO {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
}
Tre classi per la stessa cosa. Più i mapper:
// Mapper 1: DTO → Protobuf (lato MS chiamante)
public UserProto.User toProto(UserDTO dto) {
return UserProto.User.newBuilder()
.setId(dto.getId())
.setUsername(dto.getUsername() != null ? dto.getUsername() : "")
.setEmail(dto.getEmail() != null ? dto.getEmail() : "")
.setCreatedAt(dto.getCreatedAt().toString()) // conversione manuale
.build();
}
// Mapper 2: Protobuf → Entity (lato DAL)
public UserEntity toEntity(UserProto.User proto) {
UserEntity entity = new UserEntity();
entity.setId(proto.getId());
// Proto3: stringa vuota = valore default, non null
// Hibernate: null e stringa vuota sono cose diverse
entity.setUsername(proto.getUsername().isEmpty() ? null : proto.getUsername());
entity.setEmail(proto.getEmail().isEmpty() ? null : proto.getEmail());
entity.setCreatedAt(LocalDateTime.parse(proto.getCreatedAt())); // può esplodere
return entity;
}
Moltiplica questo per 50+ entità, ognuna con le sue peculiarità (relazioni JPA, tipi custom, enumerazioni, campi nullable).
I problemi concreti che ho vissuto
1. Sincronizzazione fragile tra i tre livelli
Aggiungi un campo all'entity JPA → devi aggiornare il .proto → rigenerare le classi Protobuf → aggiornare i mapper su entrambi i lati → aggiornare il DTO. Una modifica semplice diventava una cascata su 4 file in 2 progetti diversi.
2. Il problema null/default di Proto3
Protobuf 3 non ha il concetto di null. Un campo string non valorizzato vale "", un int64 vale 0. Hibernate invece distingue null da "" e da 0. Questo generava bug sottili difficili da tracciare: record che sembravano aggiornati correttamente ma perdevano dati nulli durante la serializzazione.
3. Tipi Java non supportati nativamenteLocalDateTime, BigDecimal, UUID, enumerazioni custom: nessuno ha un mapping diretto in Protobuf. Ogni tipo richiedeva una conversione manuale e una gestione dell'errore separata.
4. Il tempo di mapping è diventato insostenibile
Non parlo di performance runtime — parlo di tempo di sviluppo. Con 50+ entità e la necessità di mantenere sincronizzati tre livelli, una parte significativa del tempo di ogni sprint andava in pura attività di mapping, senza produrre logica di business.
Decisione: gRPC rimosso.
Fase 2 — REST con WebFlux: meglio, ma il problema architetturale restava
Sostituire gRPC con REST + Spring WebFlux ha eliminato il problema del triple mapping. Il flusso è diventato:
[Microservizio]
│
│ DTO (serializzato automaticamente da Jackson)
▼
WebClient (chiamata HTTP/REST non bloccante)
│
▼
[DAL Microservizio]
│
│ Jackson deserializza direttamente nel DTO
▼
Mapper DTO → Entity (un solo step)
│
▼
Spring Data JPA → Database
Un solo layer di mapping invece di due. Jackson gestisce automaticamente LocalDateTime, BigDecimal, UUID con i moduli giusti. I tipi null sono null, senza ambiguità.
// WebClient call — lato MS
public Mono<UserDTO> findById(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDTO.class); // Jackson fa tutto
}
// Controller DAL — lato server
@GetMapping("/users/{id}")
public Mono<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(userMapper::toDTO); // un solo mapping Entity → DTO
}
Miglioramento netto rispetto a gRPC: sì. Ma restava un problema di fondo.
Il problema che REST non risolve
Il DAL come microservizio separato porta con sé un costo fisso indipendente dal protocollo:
- Latenza di rete per ogni accesso ai dati, anche per operazioni semplici
- Deploy separato da coordinare ad ogni modifica delle entity
- Overhead operativo: health check, service discovery, circuit breaker tra MS e DAL
- Complessità di testing: ogni test di integrazione richiedeva il DAL attivo
Con 4+ microservizi che chiamavano il DAL, ogni operazione di business attraversava almeno due hop di rete dove prima ne bastava uno.
Fase 3 — La soluzione finale: il DAL come libreria Maven condivisa
L'insight chiave è stato chiedersi: perché il DAL deve essere un processo separato?
Un microservizio DAL ha senso quando:
- Più servizi devono scrivere sullo stesso database contemporaneamente con logica condivisa da centralizzare
- Il DAL espone dati a servizi scritti in linguaggi diversi
- Il DAL deve scalare indipendentemente dai servizi che lo usano
Nel nostro caso, nessuna di queste condizioni era vera al livello che giustificasse il costo. Il database era già condiviso, tutti i servizi erano Spring Boot, e la scalabilità del DAL non era un requisito separato.
Soluzione: il DAL diventa una libreria Maven, importata come dipendenza da tutti i microservizi che ne hanno bisogno.
Struttura della libreria
dal-library/
├── pom.xml
└── src/main/java/it/saverioriotto/dal/
├── entity/
│ ├── UserEntity.java
│ ├── OrderEntity.java
│ └── ... (tutte le @Entity)
├── repository/
│ ├── UserRepository.java // extends JpaRepository
│ ├── OrderRepository.java
│ └── ...
├── service/
│ ├── UserDalService.java // logica query complessa
│ ├── OrderDalService.java
│ └── ...
└── config/
└── DalAutoConfiguration.java // @Configuration con @EnableJpaRepositories
<!-- pom.xml della libreria -->
<groupId>it.saverioriotto</groupId>
<artifactId>dal-library</artifactId>
<version>1.2.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Nessuna dipendenza web: è solo un layer dati -->
</dependencies>
// DalAutoConfiguration.java
@Configuration
@EnableJpaRepositories(basePackages = "it.saverioriotto.dal.repository")
@EntityScan(basePackages = "it.saverioriotto.dal.entity")
public class DalAutoConfiguration {
// Spring Boot autoconfiguration: i MS importano e funziona
}
Come la usano i microservizi
<!-- pom.xml di ogni microservizio -->
<dependency>
<groupId>it.saverioriotto</groupId>
<artifactId>dal-library</artifactId>
<version>1.2.0</version>
</dependency>
// Dentro il microservizio: zero infrastruttura, accesso diretto
@Service
public class OrderService {
private final OrderDalService orderDalService; // iniettato dalla libreria
private final UserRepository userRepository; // iniettato dalla libreria
public OrderService(OrderDalService orderDalService,
UserRepository userRepository) {
this.orderDalService = orderDalService;
this.userRepository = userRepository;
}
public List<OrderSummary> getActiveOrdersForUser(Long userId) {
// chiamata diretta, nessuna rete, nessun mapping extra
return orderDalService.findActiveByUserId(userId);
}
}
Il confronto finale
| Aspetto | gRPC + DAL MS | REST/WebFlux + DAL MS | Libreria Maven |
|---|---|---|---|
| Layer di mapping | 3 (DTO↔Proto↔Entity) | 2 (DTO↔Entity) | 1 (Entity→DTO opzionale) |
| Latenza per chiamata | Rete + serializzazione binaria | Rete + JSON | Zero (in-process) |
| Deploy coordinati | Sì | Sì | No (versionamento Maven) |
| Gestione null/tipi | Problematica (Proto3) | Trasparente (Jackson) | Trasparente (Java nativo) |
| Testing | Complesso (mock del DAL MS) | Complesso (mock del DAL MS) | Semplice (H2 in-memory) |
| Overhead operativo | Alto | Medio | Nessuno |
| Condivisione tra MS | Sì (via rete) | Sì (via rete) | Sì (via dipendenza Maven) |
Quando allora ha senso usare gRPC?
Dopo questa esperienza, la mia risposta è più sfumata e onesta.
gRPC vale il costo quando:
- Hai team diversi che lavorano su servizi in linguaggi diversi (Go, Python, Java) e hai bisogno di un contratto forte come source of truth
- Hai volumi altissimi di chiamate sincrone (migliaia/secondo) dove la performance del protocollo fa davvero differenza misurabile
- Hai bisogno di streaming bidirezionale che REST non può offrire
- Il tuo team ha già esperienza con Protobuf e il tooling è maturo nell'organizzazione
gRPC non vale il costo quando:
- Sei in un ambiente mono-tecnologia (tutti Spring Boot): OpenAPI + Jackson ti dà già un contratto sufficientemente forte
- Il tuo DAL è interno e non esposto a team o linguaggi esterni
- Le tue entità cambiano frequentemente: ogni modifica al
.protoè una mini-release da coordinare - Il team è piccolo e il tempo di sviluppo è la risorsa più scarsa
Conclusione
L'architettura giusta non è quella con la tecnologia più moderna. È quella con il costo totale più basso rispetto al valore prodotto.
gRPC ha un posto preciso nell'ecosistema enterprise, ma quel posto non è "tra i microservizi interni di un team Java che condividono lo stesso dominio dati". In quel contesto, paga un prezzo alto in complessità e tempo di sviluppo senza portare vantaggi reali.
La libreria Maven condivisa non fa notizia. Non ha un logo figo. Non finisce nei talk delle conferenze. Ma in produzione, con 4+ microservizi e 50+ entità, ha ridotto il boilerplate, eliminato la latenza di rete tra servizi interni e semplificato il testing in modo significativo.
A volte la soluzione migliore è quella che puoi spiegare in un pom.xml.
Hai adottato un approccio simile o diverso nella gestione del DAL in un'architettura a microservizi? Scrivilo nei commenti o nel forum — mi interessa confrontarmi su casi reali.
Lascia un commento