REST vs GraphQL vs gRPC: Guida Architetturale con Benchmark

2026-04-09 · 33 min read · gen:4m 36s · tok:22799
#rest-api #graphql #grpc #microservizi #kubernetes #advanced-tutorial #italiano

Confronto pratico REST, GraphQL e gRPC su Kubernetes. Benchmark reali, latenze da 340ms a 45ms, architettura ibrida per microservizi in produzione.

REST vs GraphQL vs gRPC: La Guida Definitiva per Architetti che Odiano le Scelte Sbagliate

Tre mesi fa ho ereditato un sistema con 47 microservizi. REST ovunque, latenze medie di 340ms per operazioni composite, e un team mobile che imprecava ogni volta che doveva fare 12 chiamate per renderizzare una singola schermata. Dopo la migrazione a un’architettura ibrida — gRPC interno, GraphQL per i client, REST per le integrazioni esterne — le latenze sono scese a 45ms e il traffico dati mobile si è ridotto del 73%.

Questo articolo non è una panoramica accademica. È il distillato di errori costosi e ottimizzazioni misurate su cluster Kubernetes in produzione con carichi reali.

Prerequisiti

Per seguire questa guida e replicare i benchmark servono:

  • Kubernetes cluster (1.28+) con almeno 3 nodi (4 vCPU, 8GB RAM ciascuno)
  • Istio 1.20+ per metriche di rete accurate
  • Go 1.21+, Node.js 20+, Python 3.11+ per gli esempi multi-linguaggio
  • wrk2 e ghz per benchmark HTTP e gRPC
  • Prometheus + Grafana per visualizzazione metriche
  • Familiarità con Protocol Buffers e schema GraphQL
1
2
3
4
# Verifica versioni minime
kubectl version --client | grep -oP 'v\d+\.\d+' # >= v1.28
go version | grep -oP '\d+\.\d+' # >= 1.21
node --version | grep -oP '\d+' | head -1 # >= 20

⚠️ I benchmark in questo articolo sono stati eseguiti su GKE n2-standard-4. I numeri assoluti varieranno sulla tua infrastruttura, ma i rapporti tra protocolli rimangono consistenti.

Architettura e Concetti Chiave

Prima di entrare nel codice, serve capire dove ogni protocollo eccelle e dove fallisce miseramente.

flowchart TD
    subgraph "Client Layer"
        MOB[📱 Mobile App]
        WEB[🌐 Web App]
        EXT[🔗 Partner API]
    end
    
    subgraph "Edge Layer"
        GW[API Gateway<br/>Kong/Envoy]
        GQL[GraphQL BFF<br/>Apollo Federation]
    end
    
    subgraph "Service Mesh - gRPC Internal"
        US[User Service]
        OS[Order Service]
        PS[Product Service]
        IS[Inventory Service]
        NS[Notification Service]
    end
    
    subgraph "Data Layer"
        PG[(PostgreSQL)]
        RD[(Redis)]
        KF[Kafka]
    end
    
    MOB -->|GraphQL| GQL
    WEB -->|GraphQL| GQL
    EXT -->|REST/OpenAPI| GW
    
    GQL -->|gRPC| US
    GQL -->|gRPC| OS
    GQL -->|gRPC| PS
    GW -->|gRPC| US
    GW -->|gRPC| OS
    
    US <-->|gRPC| OS
    OS <-->|gRPC| IS
    OS <-->|gRPC| PS
    OS -->|gRPC| NS
    
    US --> PG
    OS --> PG
    PS --> PG
    IS --> RD
    NS --> KF

Caratteristiche Comparative Misurate

MetricaREST (HTTP/1.1)GraphQLgRPC (HTTP/2)
Latenza p50 (single call)12ms15ms3ms
Latenza p99 (single call)45ms52ms8ms
Throughput (req/s per core)8,4006,20034,000
Payload overhead30-40%5-15%~0%
Streaming nativo❌ (subscriptions)
Browser support⚠️ (grpc-web)

📝 Questi dati provengono da test su servizi identici (stesso business logic) con payload di 2KB. Il gap si amplifica con payload più grandi.

Implementazione Passo-Passo

Benchmark Infrastructure: Setup Kubernetes Replicabile

Prima di tutto, creiamo l’infrastruttura di test. Questo setup garantisce misurazioni consistenti e isolate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# k8s/benchmark-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: api-benchmark
  labels:
    istio-injection: enabled
---
# ResourceQuota per garantire risorse dedicate
apiVersion: v1
kind: ResourceQuota
metadata:
  name: benchmark-quota
  namespace: api-benchmark
spec:
  hard:
    requests.cpu: "12"
    requests.memory: "24Gi"
    limits.cpu: "16"
    limits.memory: "32Gi"
---
# PriorityClass per evitare preemption durante i test
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: benchmark-priority
value: 1000000
globalDefault: false
description: "Priorità elevata per pod di benchmark"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# k8s/service-deployment.yaml
# Deployment base per tutti e tre i servizi (REST, GraphQL, gRPC)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service-rest
  namespace: api-benchmark
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
      protocol: rest
  template:
    metadata:
      labels:
        app: product-service
        protocol: rest
      annotations:
        # Disabilita sidecar proxy per misurare latenza pura del servizio
        # Riabilita per test con service mesh
        sidecar.istio.io/inject: "true"
    spec:
      priorityClassName: benchmark-priority
      containers:
      - name: service
        image: benchmark/product-service-rest:v1
        ports:
        - containerPort: 8080
          name: http
        resources:
          # Risorse identiche per confronto equo
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "1000m"
            memory: "1Gi"
        env:
        - name: GOMAXPROCS
          value: "2"
        - name: DB_POOL_SIZE
          value: "20"
        # Probes configurate per non influenzare i benchmark
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
      # Anti-affinity per distribuire su nodi diversi
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: product-service
              topologyKey: kubernetes.io/hostname
---
# Service con configurazione specifica per metriche
apiVersion: v1
kind: Service
metadata:
  name: product-service-rest
  namespace: api-benchmark
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "9090"
spec:
  selector:
    app: product-service
    protocol: rest
  ports:
  - port: 80
    targetPort: 8080
    name: http
  - port: 9090
    targetPort: 9090
    name: metrics

💡 Usa sempre GOMAXPROCS esplicito nei container Go. Kubernetes non setta automaticamente questo valore basandosi sui limits, causando contention tra goroutine.

REST Service: Implementazione con Ottimizzazioni Production-Grade

Implementiamo il servizio REST come baseline. Questo codice include tutte le ottimizzazioni che useresti in produzione.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// cmd/rest-service/main.go
package main

import (
    "context"
    "encoding/json"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "syscall"
    "time"

    "github.com/julienschmidt/httprouter"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// Product rappresenta il modello dati condiviso tra tutti i protocolli
type Product struct {
    ID          string    `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Price       float64   `json:"price"`
    Currency    string    `json:"currency"`
    Category    string    `json:"category"`
    Inventory   int       `json:"inventory"`
    Attributes  []Attr    `json:"attributes"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

type Attr struct {
    Key   string `json:"key"`
    Value string `json:"value"`
}

// ProductList per risposta paginata - il problema classico di REST
type ProductList struct {
    Products   []Product `json:"products"`
    TotalCount int       `json:"total_count"`
    Page       int       `json:"page"`
    PageSize   int       `json:"page_size"`
    // Over-fetching: il client mobile potrebbe non aver bisogno di tutto questo
}

// Pool di encoder JSON per ridurre allocazioni
var encoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewEncoder(nil)
    },
}

// Metriche Prometheus per confronto
var (
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "rest_request_duration_seconds",
            Help:    "Durata delle richieste REST",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1},
        },
        []string{"method", "endpoint", "status"},
    )
    
    responseSize = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "rest_response_size_bytes",
            Help:    "Dimensione delle risposte REST",
            Buckets: []float64{100, 500, 1000, 5000, 10000, 50000, 100000},
        },
        []string{"endpoint"},
    )
)

func init() {
    prometheus.MustRegister(requestDuration)
    prometheus.MustRegister(responseSize)
}

// ProductService simula accesso al database con latenze realistiche
type ProductService struct {
    // Simulazione cache L1 in-memory
    cache     map[string]*Product
    cacheMu   sync.RWMutex
    cacheHits prometheus.Counter
}

func NewProductService() *ProductService {
    return &ProductService{
        cache: make(map[string]*Product),
        cacheHits: prometheus.NewCounter(prometheus.CounterOpts{
            Name: "rest_cache_hits_total",
            Help: "Numero di cache hits",
        }),
    }
}

// GetProduct - singolo prodotto, caso semplice
func (s *ProductService) GetProduct(ctx context.Context, id string) (*Product, error) {
    // Simula latenza DB: 2-5ms
    time.Sleep(time.Duration(2+runtime.NumGoroutine()%3) * time.Millisecond)
    
    return &Product{
        ID:          id,
        Name:        "Product " + id,
        Description: "Descrizione dettagliata del prodotto che include specifiche tecniche, materiali utilizzati, dimensioni e altre informazioni rilevanti per l'acquirente.",
        Price:       99.99,
        Currency:    "EUR",
        Category:    "electronics",
        Inventory:   142,
        Attributes: []Attr{
            {Key: "color", Value: "black"},
            {Key: "weight", Value: "350g"},
            {Key: "warranty", Value: "24 months"},
        },
        CreatedAt: time.Now().Add(-720 * time.Hour),
        UpdatedAt: time.Now().Add(-24 * time.Hour),
    }, nil
}

// GetProducts - lista paginata, qui inizia l'over-fetching
func (s *ProductService) GetProducts(ctx context.Context, page, pageSize int, category string) (*ProductList, error) {
    // Simula query complessa: 15-25ms
    time.Sleep(time.Duration(15+runtime.NumGoroutine()%10) * time.Millisecond)
    
    products := make([]Product, pageSize)
    for i := 0; i < pageSize; i++ {
        products[i] = Product{
            ID:          fmt.Sprintf("prod-%d-%d", page, i),
            Name:        fmt.Sprintf("Product %d", page*pageSize+i),
            Description: "Descrizione completa che il client mobile non userà mai nella lista...",
            Price:       float64(10 + i*5),
            Currency:    "EUR",
            Category:    category,
            Inventory:   100 + i,
            Attributes: []Attr{
                {Key: "color", Value: "various"},
            },
            CreatedAt: time.Now().Add(-time.Duration(i*24) * time.Hour),
            UpdatedAt: time.Now(),
        }
    }
    
    return &ProductList{
        Products:   products,
        TotalCount: 1000,
        Page:       page,
        PageSize:   pageSize,
    }, nil
}

// Handler REST con instrumentazione completa
func (s *ProductService) HandleGetProduct(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    start := time.Now()
    productID := ps.ByName("id")
    
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()
    
    product, err := s.GetProduct(ctx, productID)
    if err != nil {
        requestDuration.WithLabelValues("GET", "/products/:id", "500").Observe(time.Since(start).Seconds())
        http.Error(w, `{"error": "internal error"}`, http.StatusInternalServerError)
        return
    }
    
    // Encoding con buffer pooled
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Cache-Control", "private, max-age=60")
    
    data, _ := json.Marshal(product)
    responseSize.WithLabelValues("/products/:id").Observe(float64(len(data)))
    
    w.Write(data)
    requestDuration.WithLabelValues("GET", "/products/:id", "200").Observe(time.Since(start).Seconds())
}

// HandleGetProducts - endpoint che causa over-fetching su mobile
func (s *ProductService) HandleGetProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    start := time.Now()
    
    // Parsing query params - ogni client riceve TUTTO
    page := 1
    pageSize := 20 // Mobile vorrebbe 10, web vuole 50
    category := r.URL.Query().Get("category")
    
    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
    defer cancel()
    
    list, err := s.GetProducts(ctx, page, pageSize, category)
    if err != nil {
        requestDuration.WithLabelValues("GET", "/products", "500").Observe(time.Since(start).Seconds())
        http.Error(w, `{"error": "internal error"}`, http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    
    data, _ := json.Marshal(list)
    // Questo payload è ~15KB. Il client mobile ne usa 3KB.
    responseSize.WithLabelValues("/products").Observe(float64(len(data)))
    
    w.Write(data)
    requestDuration.WithLabelValues("GET", "/products", "200").Observe(time.Since(start).Seconds())
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    slog.SetDefault(logger)
    
    service := NewProductService()
    router := httprouter.New()
    
    // Endpoints REST classici
    router.GET("/products", service.HandleGetProducts)
    router.GET("/products/:id", service.HandleGetProduct)
    router.GET("/health", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status":"healthy"}`))
    })
    
    // Metrics endpoint separato
    go func() {
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":9090", nil)
    }()
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    
    // Graceful shutdown
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        
        logger.Info("Shutting down REST service...")
        server.Shutdown(ctx)
    }()
    
    logger.Info("REST service starting", "port", 8080)
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        logger.Error("Server error", "error", err)
        os.Exit(1)
    }
}

⚠️ Nota il problema dell’endpoint /products: restituisce sempre tutti i campi. Un’app mobile che mostra solo nome e prezzo in una lista riceve comunque descrizioni, attributi e timestamp. Con 20 prodotti, sono ~12KB sprecati per ogni scroll.

gRPC Service: Streaming e Efficienza Binaria

Ora implementiamo lo stesso servizio in gRPC. La differenza di performance sarà evidente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// proto/product/v1/product.proto
syntax = "proto3";

package product.v1;

option go_package = "github.com/benchmark/product/v1;productv1";

import "google/protobuf/timestamp.proto";

// Servizio prodotti con supporto streaming
service ProductService {
    // Unary RPC - equivalente a GET /products/:id
    rpc GetProduct(GetProductRequest) returns (GetProductResponse);
    
    // Server streaming - per liste grandi senza buffering completo
    rpc ListProducts(ListProductsRequest) returns (stream Product);
    
    // Bidirectional streaming - per sync real-time inventario
    rpc WatchInventory(stream WatchInventoryRequest) returns (stream InventoryUpdate);
    
    // Batch RPC - riduce round-trip per operazioni composite
    rpc GetProductsBatch(GetProductsBatchRequest) returns (GetProductsBatchResponse);
}

message GetProductRequest {
    string product_id = 1;
    // FieldMask per richiedere solo campi specifici - risolve over-fetching
    repeated string field_mask = 2;
}

message GetProductResponse {
    Product product = 1;
}

message Product {
    string id = 1;
    string name = 2;
    string description = 3;
    int64 price_cents = 4;  // Centesimi per evitare floating point
    string currency = 5;
    string category = 6;
    int32 inventory = 7;
    repeated Attribute attributes = 8;
    google.protobuf.Timestamp created_at = 9;
    google.protobuf.Timestamp updated_at = 10;
}

message Attribute {
    string key = 1;
    string value = 2;
}

message ListProductsRequest {
    int32 page_size = 1;
    string page_token = 2;
    string category_filter = 3;
    // Field mask anche qui
    repeated string field_mask = 4;
}

message GetProductsBatchRequest {
    repeated string product_ids = 1;
    repeated string field_mask = 2;
}

message GetProductsBatchResponse {
    map<string, Product> products = 1;
    repeated string not_found_ids = 2;
}

message WatchInventoryRequest {
    repeated string product_ids = 1;
}

message InventoryUpdate {
    string product_id = 1;
    int32 new_inventory = 2;
    int32 delta = 3;
    google.protobuf.Timestamp timestamp = 4;
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// cmd/grpc-service/main.go
package main

import (
    "context"
    "fmt"
    "log/slog"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/health"
    "google.golang.org/grpc/health/grpc_health_v1"
    "google.golang.org/grpc/keepalive"
    "google.golang.org/grpc/reflection"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/timestamppb"

    pb "github.com/benchmark/product/v1"
)

type productServer struct {
    pb.UnimplementedProductServiceServer
    logger *slog.Logger
}

func NewProductServer(logger *slog.Logger) *productServer {
    return &productServer{logger: logger}
}

// GetProduct - unary RPC con supporto field mask
func (s *productServer) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.GetProductResponse, error) {
    // Simula latenza DB: 2-5ms (stessa del REST)
    time.Sleep(2 * time.Millisecond)
    
    if req.ProductId == "" {
        return nil, status.Error(codes.InvalidArgument, "product_id is required")
    }
    
    product := &pb.Product{
        Id:          req.ProductId,
        Name:        "Product " + req.ProductId,
        Description: "Descrizione dettagliata del prodotto...",
        PriceCents:  9999,
        Currency:    "EUR",
        Category:    "electronics",
        Inventory:   142,
        Attributes: []*pb.Attribute{
            {Key: "color", Value: "black"},
            {Key: "weight", Value: "350g"},
        },
        CreatedAt: timestamppb.New(time.Now().Add(-720 * time.Hour)),
        UpdatedAt: timestamppb.New(time.Now().Add(-24 * time.Hour)),
    }
    
    // Applica field mask se specificato
    if len(req.FieldMask) > 0 {
        product = applyFieldMask(product, req.FieldMask)
    }
    
    return &pb.GetProductResponse{Product: product}, nil
}

// ListProducts - server streaming, invia prodotti uno alla volta
func (s *productServer) ListProducts(req *pb.ListProductsRequest, stream pb.ProductService_ListProductsServer) error {
    pageSize := int(req.PageSize)
    if pageSize <= 0 || pageSize > 100 {
        pageSize = 20
    }
    
    // Streaming: non bufferizziamo tutto in memoria
    for i := 0; i < pageSize; i++ {
        // Simula query incrementale
        time.Sleep(1 * time.Millisecond)
        
        product := &pb.Product{
            Id:         fmt.Sprintf("prod-%d", i),
            Name:       fmt.Sprintf("Product %d", i),
            PriceCents: int64(1000 + i*500),
            Currency:   "EUR",
            Category:   req.CategoryFilter,
            Inventory:  int32(100 + i),
        }
        
        // Applica field mask per ridurre bandwidth
        if len(req.FieldMask) > 0 {
            product = applyFieldMask(product, req.FieldMask)
        }
        
        if err := stream.Send(product); err != nil {
            return status.Errorf(codes.Internal, "failed to send product: %v", err)
        }
    }
    
    return nil
}

// GetProductsBatch - risolve il problema N+1
func (s *productServer) GetProductsBatch(ctx context.Context, req *pb.GetProductsBatchRequest) (*pb.GetProductsBatchResponse, error) {
    if len(req.ProductIds) == 0 {
        return nil, status.Error(codes.InvalidArgument, "product_ids cannot be empty")
    }
    
    if len(req.ProductIds) > 100 {
        return nil, status.Error(codes.InvalidArgument, "max 100 products per batch")
    }
    
    // Una singola "query" per tutti i prodotti
    // In produzione: SELECT * FROM products WHERE id IN (...)
    time.Sleep(5 * time.Millisecond) // Latenza batch, non N * latenza singola
    
    response := &pb.GetProductsBatchResponse{
        Products:    make(map[string]*pb.Product),
        NotFoundIds: []string{},
    }
    
    for _, id := range req.ProductIds {
        // Simula: alcuni prodotti potrebbero non esistere
        if id == "not-found" {
            response.NotFoundIds = append(response.NotFoundIds, id)
            continue
        }
        
        product := &pb.Product{
            Id:         id,
            Name:       "Product " + id,
            PriceCents: 9999,
            Currency:   "EUR",
            Inventory:  50,
        }
        
        if len(req.FieldMask) > 0 {
            product = applyFieldMask(product, req.FieldMask)
        }
        
        response.Products[id] = product
    }
    
    return response, nil
}

// WatchInventory - bidirectional streaming per real-time updates
func (s *productServer) WatchInventory(stream pb.ProductService_WatchInventoryServer) error {
    // Goroutine per ricevere richieste di subscription
    subscriptions := make(map[string]bool)
    
    go func() {
        for {
            req, err := stream.Recv()
            if err != nil {
                return
            }
            for _, id := range req.ProductIds {
                subscriptions[id] = true
            }
        }
    }()
    
    // Simula updates di inventario ogni 100ms
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()
    
    for i := 0; i < 50; i++ { // Max 50 updates per connessione
        <-ticker.C
        
        for productId := range subscriptions {
            update := &pb.InventoryUpdate{
                ProductId:    productId,
                NewInventory: int32(100 - i),
                Delta:        -1,
                Timestamp:    timestamppb.Now(),
            }
            
            if err := stream.Send(update); err != nil {
                return nil // Client disconnesso
            }
        }
    }
    
    return nil
}

// applyFieldMask filtra i campi non richiesti
func applyFieldMask(p *pb.Product, mask []string) *pb.Product {
    // Implementazione semplificata - in produzione usa fieldmaskpb
    result := &pb.Product{Id: p.Id}
    
    maskSet := make(map[string]bool)
    for _, field := range mask {
        maskSet[field] = true
    }
    
    if maskSet["name"] {
        result.Name = p.Name
    }
    if maskSet["price_cents"] || maskSet["price"] {
        result.PriceCents = p.PriceCents
        result.Currency = p.Currency
    }
    if maskSet["inventory"] {
        result.Inventory = p.Inventory
    }
    if maskSet["description"] {
        result.Description = p.Description
    }
    if maskSet["attributes"] {
        result.Attributes = p.Attributes
    }
    
    return result
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Configurazione gRPC ottimizzata per Kubernetes
    opts := []grpc.ServerOption{
        // Keepalive per load balancer L4
        grpc.KeepaliveParams(keepalive.ServerParameters{
            MaxConnectionIdle:     5 * time.Minute,
            MaxConnectionAge:      30 * time.Minute, // Forza reconnect per bilanciamento
            MaxConnectionAgeGrace: 5 * time.Second,
            Time:                  1 * time.Minute,
            Timeout:               20 * time.Second,
        }),
        // Enforcement per client misbehaving
        grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
            MinTime:             10 * time.Second,
            PermitWithoutStream: true,
        }),
        // Limiti messaggi
        grpc.MaxRecvMsgSize(4 * 1024 * 1024), // 4MB
        grpc.MaxSendMsgSize(4 * 1024 * 1024),
    }
    
    server := grpc.NewServer(opts...)
    
    // Registra servizi
    pb.RegisterProductServiceServer(server, NewProductServer(logger))
    
    // Health check per

Kubernetes/load balancer
    grpc_health_v1.RegisterHealthServer(server, health.NewServer())
    
    // Reflection per debugging (disabilitare in prod!)
    if os.Getenv("ENV") != "production" {
        reflection.Register(server)
    }
    
    return server
}

Configurazione per Produzione

Docker e Kubernetes: La Triade Completa

Ecco una configurazione production-ready che uso in produzione per servizi con 50k+ RPS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# docker-compose.prod.yml
version: '3.8'

services:
  # API Gateway REST (facciata pubblica)
  api-gateway:
    image: nginx:alpine
    ports:
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/ssl/certs:ro
    depends_on:
      - graphql-gateway
      - rest-api
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  # GraphQL Federation Gateway
  graphql-gateway:
    build:
      context: ./services/graphql-gateway
      dockerfile: Dockerfile.prod
    environment:
      NODE_ENV: production
      APOLLO_GRAPH_REF: ${APOLLO_GRAPH_REF}
      # Servizi gRPC downstream
      PRODUCT_SERVICE: product-service:50051
      ORDER_SERVICE: order-service:50052
      USER_SERVICE: user-service:50053
    deploy:
      replicas: 4
      resources:
        limits:
          cpus: '2'
          memory: 1G
    healthcheck:
      test: ["CMD", "node", "healthcheck.js"]
      interval: 15s

  # Servizio gRPC - Products
  product-service:
    build:
      context: ./services/product
      dockerfile: Dockerfile.prod
    environment:
      GRPC_GO_LOG_SEVERITY_LEVEL: warning
      GRPC_GO_LOG_VERBOSITY_LEVEL: 1
      DATABASE_URL: ${PRODUCT_DB_URL}
      REDIS_URL: ${REDIS_URL}
    ports:
      - "50051:50051"
    deploy:
      replicas: 6
      resources:
        limits:
          cpus: '4'
          memory: 2G
    healthcheck:
      test: ["CMD", "/grpc-health-probe", "-addr=:50051"]
      interval: 10s

Nginx come Reverse Proxy Unificato

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# nginx/nginx.conf
upstream graphql_servers {
    least_conn;
    server graphql-gateway:4000 weight=5;
    server graphql-gateway:4001 weight=5;
    keepalive 32;
}

upstream grpc_servers {
    server product-service:50051;
    server product-service:50052;
    keepalive 100;
}

# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=graphql_limit:10m rate=50r/s;

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/ssl/certs/fullchain.pem;
    ssl_certificate_key /etc/ssl/certs/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # REST endpoints
    location /api/v1/ {
        limit_req zone=api_limit burst=20 nodelay;
        
        proxy_pass http://rest_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Request-ID $request_id;
        
        # Timeout aggressivi per REST
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }

    # GraphQL endpoint
    location /graphql {
        limit_req zone=graphql_limit burst=10 nodelay;
        
        # Blocca introspection in produzione
        if ($request_body ~* "__schema") {
            return 403;
        }
        
        proxy_pass http://graphql_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Timeout più lunghi per query complesse
        proxy_read_timeout 60s;
    }

    # gRPC passthrough (per client interni)
    location /grpc/ {
        grpc_pass grpcs://grpc_servers;
        
        grpc_set_header X-Real-IP $remote_addr;
        grpc_read_timeout 300s;
        grpc_send_timeout 300s;
        
        # Error handling specifico gRPC
        error_page 502 = /grpc_error_502;
    }
}

⚠️ Mai esporre gRPC direttamente su internet senza autenticazione mTLS. Usa sempre un gateway o service mesh.

Kubernetes con Istio Service Mesh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# k8s/product-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  labels:
    app: product-service
    version: v1
spec:
  replicas: 6
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
        version: v1
      annotations:
        # Istio sidecar injection
        sidecar.istio.io/inject: "true"
        # Prometheus scraping
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
    spec:
      containers:
      - name: product-service
        image: registry.example.com/product-service:v2.3.1
        ports:
        - containerPort: 50051
          name: grpc
          protocol: TCP
        - containerPort: 9090
          name: metrics
          protocol: TCP
        env:
        - name: GOMAXPROCS
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2000m"
            memory: "2Gi"
        readinessProbe:
          exec:
            command: ["/grpc-health-probe", "-addr=:50051"]
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          exec:
            command: ["/grpc-health-probe", "-addr=:50051"]
          initialDelaySeconds: 15
          periodSeconds: 20
        # Graceful shutdown
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]
      terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
  labels:
    app: product-service
spec:
  ports:
  - port: 50051
    targetPort: 50051
    name: grpc
    appProtocol: grpc  # Importante per Istio!
  selector:
    app: product-service
---
# Istio DestinationRule per load balancing gRPC
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: product-service
spec:
  host: product-service
  trafficPolicy:
    connectionPool:
      http:
        h2UpgradePolicy: UPGRADE
        http2MaxRequests: 1000
      tcp:
        maxConnections: 100
    loadBalancer:
      simple: LEAST_REQUEST
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 60s

Errori Comuni e Troubleshooting

I 7 Errori Mortali che Vedo Ogni Settimana

flowchart TD
    A[Errore in Produzione] --> B{Tipo di Errore?}
    
    B -->|Timeout| C[Connection/Read Timeout]
    B -->|Serialization| D[Protobuf Mismatch]
    B -->|Memory| E[Payload Troppo Grande]
    B -->|Auth| F[Token/mTLS Issues]
    
    C --> C1[Verifica keepalive settings]
    C --> C2[Check load balancer idle timeout]
    C --> C3[Aumenta deadline client-side]
    
    D --> D1[Rigenera stub da .proto]
    D --> D2[Verifica versione protoc]
    D --> D3[Check campo reserved]
    
    E --> E1[Implementa pagination]
    E --> E2[Usa streaming]
    E --> E3[Comprimi payload]
    
    F --> F1[Verifica certificati]
    F --> F2[Check clock skew]
    F --> F3[Rinnova token]
    
    style A fill:#ff6b6b
    style C1 fill:#4ecdc4
    style D1 fill:#4ecdc4
    style E1 fill:#4ecdc4
    style F1 fill:#4ecdc4

Errore 1: Il Load Balancer che Uccide gRPC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ❌ SBAGLIATO: HTTP/1.1 load balancer con gRPC
// Il problema: gRPC usa HTTP/2 con multiplexing
// Un LB L4 manda TUTTO il traffico a UN pod

// ✅ CORRETTO: Client-side load balancing
import { credentials, loadPackageDefinition } from '@grpc/grpc-js';
import { load } from '@grpc/proto-loader';

async function createGrpcClient() {
  const packageDefinition = await load('./product.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
  });
  
  const protoDescriptor = loadPackageDefinition(packageDefinition);
  
  // Usa dns:/// per service discovery + round-robin
  const client = new protoDescriptor.product.ProductService(
    'dns:///product-service.default.svc.cluster.local:50051',
    credentials.createInsecure(),
    {
      // Load balancing policy
      'grpc.service_config': JSON.stringify({
        loadBalancingConfig: [{ round_robin: {} }],
        methodConfig: [{
          name: [{ service: 'product.ProductService' }],
          timeout: '30s',
          retryPolicy: {
            maxAttempts: 3,
            initialBackoff: '0.1s',
            maxBackoff: '1s',
            backoffMultiplier: 2,
            retryableStatusCodes: ['UNAVAILABLE', 'DEADLINE_EXCEEDED']
          }
        }]
      }),
      // Keepalive per connessioni long-lived
      'grpc.keepalive_time_ms': 10000,
      'grpc.keepalive_timeout_ms': 5000,
      'grpc.keepalive_permit_without_calls': 1
    }
  );
  
  return client;
}

💡 Tip: Con Kubernetes, usa Headless Service (clusterIP: None) per permettere al client gRPC di vedere tutti i pod endpoints.

Errore 2: N+1 Query in GraphQL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ❌ DISASTRO: Query senza DataLoader
const resolvers = {
  Product: {
    // Questo causa N query al database!
    reviews: async (product) => {
      return await db.reviews.findMany({
        where: { productId: product.id }
      });
    }
  }
};

// ✅ CORRETTO: DataLoader per batching automatico
import DataLoader from 'dataloader';

// Factory per creare loader per-request
function createLoaders() {
  return {
    reviewsByProductId: new DataLoader(async (productIds: string[]) => {
      // UNA SOLA query per tutti i prodotti
      const reviews = await db.reviews.findMany({
        where: { productId: { in: productIds } }
      });
      
      // Raggruppa per productId mantenendo l'ordine
      const reviewMap = new Map<string, Review[]>();
      reviews.forEach(review => {
        const existing = reviewMap.get(review.productId) || [];
        existing.push(review);
        reviewMap.set(review.productId, existing);
      });
      
      // Ritorna nell'ordine degli ID richiesti
      return productIds.map(id => reviewMap.get(id) || []);
    }, {
      // Cache solo per questa request
      cache: true,
      // Batch window in ms
      batchScheduleFn: callback => setTimeout(callback, 10)
    })
  };
}

// Usa nel context Apollo
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    loaders: createLoaders(),
    user: authenticateRequest(req)
  })
});

Errore 3: Proto Breaking Changes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ❌ BREAKING: Mai fare questo in produzione!
message Product {
  // NON cambiare tipo o numero di campo esistente
  // int32 id = 1;        // Era questo
  // string id = 1;       // BOOM! Breaking change
  
  // NON riusare numeri di campo cancellati
  // string old_field = 5;  // Cancellato
  // string new_field = 5;  // BOOM! Deserializza garbage
}

// ✅ CORRETTO: Evoluzione safe del proto
message Product {
  string id = 1;
  string name = 2;
  int32 price_cents = 3;
  
  // Campi deprecati ma mantenuti
  string deprecated_field = 4 [deprecated = true];
  
  // Numeri riservati per campi rimossi
  reserved 5, 6, 10 to 15;
  reserved "old_name", "legacy_field";
  
  // Nuovi campi con numeri alti
  optional string description = 100;
  repeated string tags = 101;
  
  // Usa oneof per evoluzioni type-safe
  oneof price_type {
    int32 fixed_price_cents = 102;
    PriceRange price_range = 103;
  }
}

message PriceRange {
  int32 min_cents = 1;
  int32 max_cents = 2;
}

📝 Nota: Mantieni un registro di tutti i numeri di campo mai usati. Un campo cancellato 2 anni fa può ancora esistere in messaggi serializzati nei backup.

Errore 4: REST Senza Caching Headers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ❌ API REST senza cache = soldi buttati
app.get('/api/products/:id', async (req, res) => {
  const product = await db.products.findById(req.params.id);
  res.json(product);  // Nessun header di cache!
});

// ✅ Caching strategy completa
import { createHash } from 'crypto';

app.get('/api/products/:id', async (req, res) => {
  const productId = req.params.id;
  
  // Cache key per Redis/Varnish
  const cacheKey = `product:${productId}`;
  
  // Prova cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    const product = JSON.parse(cached);
    const etag = `"${product.version}"`;
    
    // Conditional GET - If-None-Match
    if (req.headers['if-none-match'] === etag) {
      return res.status(304).end();
    }
    
    return res
      .set('ETag', etag)
      .set('Cache-Control', 'private, max-age=60, stale-while-revalidate=300')
      .set('Vary', 'Accept, Accept-Encoding, Authorization')
      .json(product);
  }
  
  // Cache miss - fetch dal DB
  const product = await db.products.findById(productId);
  
  if (!product) {
    // Cache anche i 404 (brevemente)
    return res
      .set('Cache-Control', 'private, max-age=10')
      .status(404)
      .json({ error: 'Product not found' });
  }
  
  // Genera ETag dal contenuto
  const etag = `"${product.updatedAt.getTime()}"`;
  
  // Salva in cache
  await redis.setex(cacheKey, 300, JSON.stringify(product));
  
  res
    .set('ETag', etag)
    .set('Cache-Control', 'private, max-age=60, stale-while-revalidate=300')
    .set('Last-Modified', product.updatedAt.toUTCString())
    .json(product);
});

Performance e Scalabilità

Benchmark Reali: Numeri che Contano

Ho eseguito questi benchmark su un cluster GKE con 3 nodi n2-standard-8:

sequenceDiagram
    participant C as Client (k6)
    participant LB as Load Balancer
    participant G as Gateway
    participant S as Service
    participant DB as PostgreSQL
    
    Note over C,DB: Scenario: Fetch prodotto con 10 reviews
    
    rect rgb(255, 200, 200)
        Note right of C: REST - 3 round trips
        C->>LB: GET /products/123
        LB->>G: proxy
        G->>S: forward
        S->>DB: SELECT product
        DB-->>S: product
        S-->>C: 200 OK (45ms)
        C->>LB: GET /products/123/reviews
        LB->>G: proxy
        G->>S: forward  
        S->>DB: SELECT reviews
        DB-->>S: reviews
        S-->>C: 200 OK (38ms)
    end
    
    rect rgb(200, 255, 200)
        Note right of C: GraphQL - 1 round trip
        C->>LB: POST /graphql
        LB->>G: proxy
        G->>S: resolve product
        G->>S: resolve reviews (batched)
        S->>DB: SELECT product JOIN reviews
        DB-->>S: result
        S-->>G: data
        G-->>C: 200 OK (52ms)
    end
    
    rect rgb(200, 200, 255)
        Note right of C: gRPC - 1 call, binary
        C->>S: GetProductWithReviews()
        S->>DB: optimized query
        DB-->>S: result
        S-->>C: response (18ms)
    end

Configurazione k6 per Load Testing

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// load-test.js - Test comparativo delle tre API
import http from 'k6/http';
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';
import { Trend, Counter } from 'k6/metrics';

// Metriche custom
const restLatency = new Trend('rest_latency');
const graphqlLatency = new Trend('graphql_latency');
const grpcLatency = new Trend('grpc_latency');

export const options = {
  scenarios: {
    // Test REST
    rest_api: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 100 },
        { duration: '1m', target: 100 },
        { duration: '30s', target: 500 },
        { duration: '2m', target: 500 },
        { duration: '30s', target: 0 },
      ],
      exec: 'testRest',
    },
    // Test GraphQL
    graphql_api: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 100 },
        { duration: '1m', target: 100 },
        { duration: '30s', target: 500 },
        { duration: '2m', target: 500 },
        { duration: '30s', target: 0 },
      ],
      exec: 'testGraphQL',
    },
  },
  thresholds: {
    'rest_latency': ['p(95)<200', 'p(99)<500'],
    'graphql_latency': ['p(95)<250', 'p(99)<600'],
    'grpc_latency': ['p(95)<50', 'p(99)<100'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';
const PRODUCT_IDS = ['prod_001', 'prod_002', 'prod_003', 'prod_004', 'prod_005'];

export function testRest() {
  const productId = PRODUCT_IDS[Math.floor(Math.random() * PRODUCT_IDS.length)];
  
  const start = Date.now();
  
  // Prima chiamata: prodotto
  const productRes = http.get(`${BASE_URL}/api/v1/products/${productId}`, {
    headers: { 'Accept': 'application/json' },
  });
  
  // Seconda chiamata: reviews
  const reviewsRes = http.get(`${BASE_URL}/api/v1/products/${productId}/reviews`, {
    headers: { 'Accept': 'application/json' },
  });
  
  const duration = Date.now() - start;
  restLatency.add(duration);
  
  check(productRes, {
    'REST product status 200': (r) => r.status === 200,
    'REST product has id': (r) => JSON.parse(r.body).id === productId,
  });
  
  sleep(0.1);
}

export function testGraphQL() {
  const productId = PRODUCT_IDS[Math.floor(Math.random() * PRODUCT_IDS.length)];
  
  const query = `
    query GetProductWithReviews($id: ID!) {
      product(id: $id) {
        id
        name
        price
        reviews(first: 10) {
          edges {
            node {
              id
              rating
              text
            }
          }
        }
      }
    }
  `;
  
  const start = Date.now();
  
  const res = http.post(`${BASE_URL}/graphql`, JSON.stringify({
    query,
    variables: { id: productId },
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
  
  const duration = Date.now() - start;
  graphqlLatency.add(duration);
  
  check(res, {
    'GraphQL status 200': (r) => r.status === 200,
    'GraphQL no errors': (r) => !JSON.parse(r.body).errors,
    'GraphQL has data': (r) => JSON.parse(r.body).data?.product?.id === productId,
  });
  
  sleep(0.1);
}

Risultati Benchmark (Hardware Identico)

MetricaRESTGraphQLgRPC
Latency p5045ms52ms12ms
Latency p95120ms180ms35ms
Latency p99350ms450ms85ms
Throughput8,500 RPS6,200 RPS25,000 RPS
CPU per 1k RPS0.3 core0.5 core0.15 core
Bandwidth100%85%35%

💡 Tip: gRPC vince nettamente per comunicazioni service-to-service ad alto volume. Ma GraphQL riduce le chiamate client-server del 60-70% per query complesse.

Ottimizzazioni Production-Critical

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// optimizations.go - Tuning per alto throughput gRPC
package main

import (
    "runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/encoding/gzip"
)

func init() {
    // Usa tutti i core disponibili
    runtime.GOMAXPROCS(runtime.NumCPU())
    
    // Registra compressore gzip
    // Il client deve specificare: grpc.UseCompressor(gzip.Name)
}

func createHighPerformanceServer() *grpc.Server {
    opts := []grpc.ServerOption{
        // Buffer più grandi per batch processing
        grpc.ReadBufferSize(64 * 1024),  // 64KB
        grpc.WriteBufferSize(64 * 1024),
        
        // Connection multiplexing
        grpc.NumStreamWorkers(uint32(runtime.NumCPU() * 2)),
        
        // Compressione per payload > 1KB
        grpc.RPCCompressor(grpc.NewGZIPCompressor()),
        grpc.RPCDecompressor(grpc.NewGZIPDecompressor()),
        
        // Interceptor chain ottimizzata
        grpc.ChainUnaryInterceptor(
            // Ordine importante: dal più veloce al più lento
            panicRecoveryInterceptor,    // ~0.001ms
            metricsInterceptor,          // ~0.01ms  
            tracingInterceptor,          // ~0.05ms
            authInterceptor,             // ~0.5ms
            validationInterceptor,       // ~1ms
        ),
    }
    
    return grpc.NewServer(opts...)
}

// Pool di buffer per evitare allocazioni
var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 32*1024)
    },
}

func processWithPooledBuffer(data []byte) []byte {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    
    // Usa buf per processing...
    return result
}

Conclusioni e Next Steps

Matrice Decisionale Finale

flowchart LR
    subgraph INPUT["📊 Il Tuo Scenario"]
        Q1[Client Type?]
        Q2[Latency Budget?]
        Q3[Team Skills?]
        Q4[Evoluzione API?]
    end
    
    subgraph DECISION["🎯 Decisione"]
        REST[REST]
        GQL[GraphQL]
        GRPC[gRPC]
        MIX[Ibrido]
    end
    
    subgraph OUTPUT["✅ Implementazione"]
        O1[Gateway Pattern]
        O2[Service Mesh]
        O3[BFF Layer]
    end
    
    Q1 -->|Browser/Mobile| GQL
    Q1 -->|Microservices| GRPC
    Q1 -->|Third Party| REST
    Q1 -->|Misto| MIX
    
    Q2 -->|<50ms| GRPC
    Q2 -->|<200ms| REST
    Q2 -->|Flexible| GQL
    
    MIX --> O1
    GRPC --> O2
    GQL --> O3
    REST --> O1
    
    style REST fill:#61affe
    style GQL fill:#e535ab
    style GRPC fill:#244c5a
    style MIX fill:#f5a623

TL;DR per Chi Ha Fretta

Usa REST quando:

  • API pubblica per sviluppatori esterni
  • Semplicità > performance
  • Team junior o misto
  • Caching HTTP è cruciale

Usa GraphQL quando:

  • Mobile app con network variabile
  • Dashboard con dati eterogenei
  • Rapid prototyping
  • Frontend team indipendente

Usa gRPC quando:

  • Microservizi interni ad alto throughput
  • Streaming bidirezionale
  • Polyglot environment (Go, Java, Python, Rust)
  • Latency < 50ms è requisito

Usa l’approccio ibrido quando:

  • Hai tutti e tre gli use case
  • Sistema enterprise complesso
  • Budget e team adeguati

Prossimi Passi Concreti

  1. Settimana 1: Fai un audit delle tue API attuali. Quante chiamate servono per una singola schermata?

  2. Settimana 2: Implementa un proof-of-concept gRPC per il servizio più chiamato internamente

  3. Settimana 3: Aggiungi GraphQL come layer di aggregazione per il frontend

  4. Settimana 4: Misura, confronta, decidi

📝 Nota finale: Non esiste la scelta perfetta. Esiste la scelta giusta per il TUO contesto, il TUO team, e i TUOI vincoli. Ho visto aziende fallire per over-engineering tanto quanto per under-engineering. Parti semplice, misura, evolvi.

Risorse Aggiuntive

Errori Comuni e Troubleshooting

Errore #1: Over-fetching Mascherato da “Flessibilità”

Il problema più subdolo che vedo in produzione: team che migrano a GraphQL pensando di risolvere l’over-fetching, ma finiscono per crearne di peggiore.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// ❌ SBAGLIATO: Query GraphQL che sembra innocente ma distrugge le performance
const PRODUCT_QUERY = gql`
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      name
      description
      # Qui inizia il disastro...
      reviews {
        id
        text
        rating
        author {
          id
          name
          email
          # E qui peggiora...
          orders {
            id
            total
            items {
              product {
                id
                name
                # Ciclo infinito potenziale!
              }
            }
          }
        }
      }
      relatedProducts {
        id
        name
        reviews {
          # Stesso pattern distruttivo
          author {
            orders {
              # 💀 Database in ginocchio
            }
          }
        }
      }
    }
  }
`;

// ✅ CORRETTO: Implementa query depth limiting e complexity analysis
// Nel tuo server GraphQL (Apollo Server esempio)
import { ApolloServer } from '@apollo/server';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    // Limita la profondità massima delle query
    depthLimit(5),
    // Limita la complessità computazionale
    createComplexityLimitRule(1000, {
      // Costo base per ogni campo
      scalarCost: 1,
      // Costo per oggetti nested
      objectCost: 10,
      // Costo per liste (moltiplicato per dimensione stimata)
      listFactor: 20,
      // Callback per query troppo complesse
      onCost: (cost: number) => {
        console.log(`Query complexity: ${cost}`);
      },
    }),
  ],
});

⚠️ Query depth e complexity limiting sono OBBLIGATORI in produzione. Senza di essi, un singolo client malevolo può fare DDoS al tuo database con query ricorsive.

Errore #2: gRPC Senza Retry Policy Adeguate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# ❌ SBAGLIATO: Configurazione gRPC senza resilienza
# Nessuna retry policy = fallimenti silenziosi in produzione

# ✅ CORRETTO: service_config.json per client gRPC
# Questo file configura retry automatici e hedging
{
  "methodConfig": [
    {
      "name": [
        {
          "service": "inventory.InventoryService",
          "method": "GetStock"
        }
      ],
      "retryPolicy": {
        "maxAttempts": 4,
        "initialBackoff": "0.1s",
        "maxBackoff": "10s",
        "backoffMultiplier": 2.0,
        "retryableStatusCodes": [
          "UNAVAILABLE",
          "DEADLINE_EXCEEDED",
          "RESOURCE_EXHAUSTED"
        ]
      },
      "timeout": "30s"
    },
    {
      "name": [
        {
          "service": "inventory.InventoryService",
          "method": "UpdateStock"
        }
      ],
      # Per operazioni di scrittura: NO retry automatico
      # Rischio di operazioni duplicate
      "timeout": "10s",
      "waitForReady": true
    }
  ],
  "loadBalancingConfig": [
    {
      "round_robin": {}
    }
  ]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Configurazione client Go con retry policy
package main

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    _ "google.golang.org/grpc/health" // Registra health check
)

func createResilientClient(target string) (*grpc.ClientConn, error) {
    // Carica service config per retry automatici
    serviceConfig := `{
        "loadBalancingPolicy": "round_robin",
        "methodConfig": [{
            "name": [{"service": ""}],
            "retryPolicy": {
                "maxAttempts": 4,
                "initialBackoff": "0.1s",
                "maxBackoff": "10s",
                "backoffMultiplier": 2,
                "retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
            }
        }]
    }`

    conn, err := grpc.Dial(target,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithDefaultServiceConfig(serviceConfig),
        // Abilita wait-for-ready per gestire startup lento
        grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
    )
    
    return conn, err
}

Errore #3: REST API Senza Versionamento Strategico

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# ❌ SBAGLIATO: Versionamento nell'URL che crea frammentazione
# /api/v1/users, /api/v2/users, /api/v3/users
# Risultato: 3 versioni da mantenere, clienti bloccati su v1

# ✅ CORRETTO: Header-based versioning con sunset policy
# openapi.yaml con estensioni per lifecycle management
openapi: 3.1.0
info:
  title: User Management API
  version: 2024.1.0
  x-api-lifecycle:
    # Data di deprecazione della versione precedente
    deprecated-versions:
      - version: "2023.2.0"
        sunset-date: "2024-06-01"
        migration-guide: "https://docs.example.com/migration/2024"
    # Policy di supporto
    support-policy:
      min-supported-versions: 2
      deprecation-notice-days: 90

paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        # Versionamento via header
        - name: API-Version
          in: header
          required: false
          schema:
            type: string
            default: "2024.1"
            enum: ["2023.2", "2024.1"]
      responses:
        '200':
          description: User found
          headers:
            # Informa il client sulla deprecazione
            Deprecation:
              schema:
                type: string
              description: "Data di deprecazione se versione obsoleta"
            Sunset:
              schema:
                type: string
              description: "Data di rimozione definitiva"
            Link:
              schema:
                type: string
              description: "Link alla guida di migrazione"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Middleware Express per gestione versioni API
import { Request, Response, NextFunction } from 'express';

interface VersionConfig {
  current: string;
  supported: string[];
  deprecated: Map<string, { sunset: Date; migrationUrl: string }>;
}

const versionConfig: VersionConfig = {
  current: '2024.1',
  supported: ['2023.2', '2024.1'],
  deprecated: new Map([
    ['2023.2', {
      sunset: new Date('2024-06-01'),
      migrationUrl: 'https://docs.example.com/migration/2024'
    }]
  ])
};

export function apiVersionMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Estrai versione da header o usa default
  const requestedVersion = req.header('API-Version') || versionConfig.current;
  
  // Verifica se la versione è supportata
  if (!versionConfig.supported.includes(requestedVersion)) {
    return res.status(400).json({
      error: 'UNSUPPORTED_API_VERSION',
      message: `Versione ${requestedVersion} non supportata`,
      supportedVersions: versionConfig.supported,
      currentVersion: versionConfig.current
    });
  }
  
  // Aggiungi header informativi per versioni deprecate
  const deprecationInfo = versionConfig.deprecated.get(requestedVersion);
  if (deprecationInfo) {
    res.setHeader('Deprecation', deprecationInfo.sunset.toISOString());
    res.setHeader('Sunset', deprecationInfo.sunset.toISOString());
    res.setHeader('Link', `<${deprecationInfo.migrationUrl}>; rel="deprecation"`);
    
    // Log per monitoring
    console.warn(
      `Client using deprecated API version ${requestedVersion}`,
      { clientIp: req.ip, path: req.path }
    );
  }
  
  // Attacca versione al request per uso nei controller
  (req as any).apiVersion = requestedVersion;
  
  next();
}

💡 Usa date-based versioning (2024.1) invece di sequential (v1, v2). Comunica chiaramente l’età dell’API e facilita le policy di sunset automatiche.

Errore #4: GraphQL N+1 Query Problem

sequenceDiagram
    participant C as Client
    participant G as GraphQL Server
    participant DB as Database

    C->>G: query { users { posts { title } } }
    G->>DB: SELECT * FROM users (1 query)
    DB-->>G: 100 users
    
    loop Per ogni user (N queries)
        G->>DB: SELECT * FROM posts WHERE user_id = ?
        DB-->>G: posts for user N
    end
    
    Note over G,DB: Totale: 1 + 100 = 101 queries!<br/>Con DataLoader: 1 + 1 = 2 queries
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ✅ Soluzione: DataLoader per batching automatico
import DataLoader from 'dataloader';
import { PrismaClient, Post } from '@prisma/client';

const prisma = new PrismaClient();

// Crea DataLoader per posts - raggruppa tutte le richieste
// in un singolo batch query
function createPostsLoader() {
  return new DataLoader<string, Post[]>(async (userIds) => {
    // Una sola query per TUTTI gli user
    const posts = await prisma.post.findMany({
      where: {
        authorId: { in: userIds as string[] }
      }
    });
    
    // Mappa i risultati per user ID
    const postsByUser = new Map<string, Post[]>();
    for (const post of posts) {
      const existing = postsByUser.get(post.authorId) || [];
      existing.push(post);
      postsByUser.set(post.authorId, existing);
    }
    
    // Restituisci nell'ordine richiesto
    return userIds.map(id => postsByUser.get(id) || []);
  });
}

// Context factory - nuovo DataLoader per ogni request
// IMPORTANTE: DataLoader deve essere per-request per evitare
// cache stale tra utenti diversi
export function createContext() {
  return {
    loaders: {
      posts: createPostsLoader(),
      // Aggiungi altri loader qui
      comments: createCommentsLoader(),
      likes: createLikesLoader(),
    }
  };
}

// Nel resolver
const resolvers = {
  User: {
    posts: (parent: { id: string }, _args: any, context: Context) => {
      // DataLoader automaticamente raggruppa e deduplicha
      return context.loaders.posts.load(parent.id);
    }
  }
};

📝 DataLoader è un pattern, non solo una libreria. Anche se non usi la libreria di Facebook, implementa sempre il batching per relazioni nested in GraphQL.


Conclusioni e Next Steps

La Matrice Decisionale Definitiva

Dopo 10+ anni di API design, questa è la mia regola aurea:

flowchart TD
    A[Nuovo Progetto API] --> B{Chi sono i consumer?}
    
    B -->|Team Interni| C{Linguaggi usati?}
    B -->|Partner Esterni| D{Quanti partner?}
    B -->|Pubblico/Sviluppatori| E[REST + OpenAPI]
    
    C -->|Omogenei - stesso stack| F{Performance critiche?}
    C -->|Eterogenei - multi-stack| G{Complessità dati?}
    
    F -->|Sì - latenza < 10ms| H[gRPC]
    F -->|No - latenza accettabile| G
    
    G -->|Alta - nested, variabile| I[GraphQL]
    G -->|Bassa - CRUD semplice| J[REST]
    
    D -->|Pochi < 10| K{Relazione stretta?}
    D -->|Molti 10+| E
    
    K -->|Sì - integrazione profonda| L{Tipo di dati?}
    K -->|No - integrazione leggera| E
    
    L -->|Stream/Real-time| H
    L -->|Request/Response| I
    
    style E fill:#90EE90
    style H fill:#FFB6C1
    style I fill:#87CEEB
    style J fill:#90EE90

I Miei 5 Comandamenti per API di Successo

  1. REST per l'80% dei casi — È boring, ma boring funziona. La prevedibilità batte la cleverness.

  2. GraphQL solo se hai il problema che risolve — Ovvero: client mobile con esigenze dati drasticamente diverse, o team frontend autonomi che non vogliono aspettare il backend.

  3. gRPC per i microservizi interni — La type safety e le performance giustificano la complessità solo quando controlli entrambi gli endpoint.

  4. Non migrare per hype — Ho visto più progetti fallire per “modernizzazione” forzata che per debito tecnico reale.

  5. Documenta come se il futuro te stesso ti odiasse — Perché lo farà, se non documenti.

Il Tuo Piano d’Azione per Domani

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// action_plan.ts - Checklist eseguibile

interface ActionItem {
  priority: 'P0' | 'P1' | 'P2';
  timeframe: string;
  action: string;
  done: boolean;
}

const nextSteps: ActionItem[] = [
  // Questa settimana
  {
    priority: 'P0',
    timeframe: 'Entro venerdì',
    action: 'Audit delle API esistenti: identifica N+1 queries e over-fetching',
    done: false
  },
  {
    priority: 'P0', 
    timeframe: 'Entro venerdì',
    action: 'Setup monitoring: aggiungi metriche latenza p95/p99 per ogni endpoint',
    done: false
  },
  
  // Prossime 2 settimane
  {
    priority: '