Introduzione: Deploy e Scaling Intelligente
Uno dei vantaggi principali del modular monolith e la semplicità di deployment: un singolo artefatto, una singola pipeline CI/CD, un singolo processo da monitorare. Ma semplicità non significa limitazione. Con le strategie giuste, un modular monolith può essere deployato con zero downtime, scalato in modo intelligente, e gestito con la stessa sofisticazione di un'architettura a microservizi, a una frazione del costo operativo.
In questo articolo esploreremo le strategie di deployment avanzate per il modular monolith: blue-green deployments, feature flags per disaccoppiare deploy e rilascio, auto-scaling basato su metriche, e i criteri per decidere quando estrarre un modulo come microservizio autonomo.
Cosa Imparerai in Questo Articolo
- Blue-green deployment per aggiornamenti con zero downtime
- Feature flags: disaccoppiare il deploy dal rilascio delle feature
- Auto-scaling con Kubernetes HPA e metriche custom
- Scaling contestuale: ottimizzare le risorse per modulo
- Database scaling: read replicas, connection pooling, sharding
- Quando estrarre un modulo come microservizio
- Configurazione Kubernetes di esempio per il modular monolith
Monolithic Deployment: Build Once, Deploy Everywhere
Il deployment di un modular monolith segue il principio "build once, deploy everywhere": un singolo artefatto (JAR, Docker image) viene costruito una volta e deployato in tutti gli ambienti (staging, production, DR). Questo elimina il rischio di inconsistenze tra ambienti e semplifica drasticamente la pipeline CI/CD.
# Pipeline CI/CD per modular monolith
# Un singolo artefatto per tutti gli ambienti
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: ./gradlew build
- name: Build Docker image
run: |
docker build -t ecommerce-app:$GITHUB_SHA .
- name: Push to registry
run: |
docker push registry.example.com/ecommerce-app:$GITHUB_SHA
deploy-staging:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: |
kubectl set image deployment/ecommerce \
app=registry.example.com/ecommerce-app:$GITHUB_SHA \
--namespace staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # richiede approvazione manuale
steps:
- name: Deploy to production
run: |
kubectl set image deployment/ecommerce \
app=registry.example.com/ecommerce-app:$GITHUB_SHA \
--namespace production
Blue-Green Deployment: Zero Downtime
Il blue-green deployment mantiene due ambienti identici: blue (attivo, serve il traffico) e green (inattivo, pronto per il prossimo rilascio). Il deploy avviene sull'ambiente inattivo, e lo switch del traffico e istantaneo attraverso il cambio di un load balancer o di un Service Kubernetes.
Vantaggi
- Zero downtime: lo switch e istantaneo, nessuna interruzione per gli utenti
- Rollback immediato: se qualcosa va storto, basta tornare all'ambiente precedente
- Validazione pre-switch: puoi testare il nuovo deploy sull'ambiente green prima dello switch
# Kubernetes: Blue-Green deployment con Service switch
# Blue deployment (attualmente attivo)
apiVersion: apps/v1
kind: Deployment
metadata:
name: ecommerce-blue
labels:
app: ecommerce
version: blue
spec:
replicas: 4
selector:
matchLabels:
app: ecommerce
version: blue
template:
metadata:
labels:
app: ecommerce
version: blue
spec:
containers:
- name: app
image: registry.example.com/ecommerce-app:v1.2.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
---
# Service che punta al deployment attivo
apiVersion: v1
kind: Service
metadata:
name: ecommerce-service
spec:
selector:
app: ecommerce
version: blue # Cambia in "green" per lo switch
ports:
- port: 80
targetPort: 8080
Feature Flags: Disaccoppiare Deploy e Rilascio
I feature flags permettono di deployare codice in produzione senza attivarlo immediatamente. Una feature può essere attivata gradualmente (canary release), per specifici utenti (beta testing), o per specifiche region. Questo disaccoppia completamente il momento del deploy (codice in produzione) dal momento del rilascio (feature visibile agli utenti).
// Feature Flags con Spring Boot e Unleash
@Service
class OrderServiceImpl implements OrderModuleApi {
private final FeatureFlagService featureFlags;
@Override
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
// Feature flag: nuovo sistema di pricing
if (featureFlags.isEnabled("new-pricing-engine")) {
order.applyNewPricing(pricingEngine.calculate(cmd));
} else {
order.applyLegacyPricing(cmd.getItems());
}
// Feature flag: notifica push (graduale)
if (featureFlags.isEnabled("push-notifications",
cmd.getUserId())) {
notificationModule.sendPush(order);
}
orderRepository.save(order);
return order.toDto();
}
}
// Configurazione Feature Flags con Unleash
@Configuration
class FeatureFlagConfig {
@Bean
public Unleash unleash() {
return new DefaultUnleash(
UnleashConfig.builder()
.appName("ecommerce-app")
.instanceId("instance-1")
.unleashAPI("http://unleash:4242/api")
.build()
);
}
}
Auto-Scaling con Kubernetes HPA
Il Horizontal Pod Autoscaler (HPA) di Kubernetes scala automaticamente il numero di pod basandosi su metriche di CPU, memoria, o metriche custom. Per un modular monolith, l'HPA scala l'intero applicativo in modo uniforme. E possibile configurare metriche custom basate su indicatori specifici del business come il numero di ordini al minuto.
# HPA per il modular monolith
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ecommerce-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ecommerce-app
minReplicas: 3
maxReplicas: 12
metrics:
# Scala basato su CPU
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Scala basato su metriche custom
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
Database Scaling
In un modular monolith, il database e spesso il collo di bottiglia prima del compute. Ecco le strategie di scaling per il database:
Read Replicas
Configura read replicas del database e instrada le query di lettura verso le repliche. Le operazioni di scrittura vanno al primary. Questo e particolarmente efficace con il pattern CQRS, dove il read model può essere su una replica dedicata.
Connection Pooling
Usa un connection pooler come PgBouncer per gestire efficientemente le connessioni al database. Con molte istanze del monolith, il numero di connessioni al database può crescere rapidamente. PgBouncer riduce il numero di connessioni effettive al database.
Sharding per Modulo
Se un modulo specifico ha volumi di dati molto superiori agli altri, puoi spostare le sue tabelle su un database dedicato. Questo e un passo intermedio verso l'estrazione come microservizio.
Quando Estrarre un Modulo come Microservizio
Il modular monolith non deve necessariamente rimanere un monolith per sempre. Quando i dati lo giustificano, un modulo può essere estratto come microservizio indipendente. Ecco i criteri decisivi:
Criteri Quantitativi
- Scaling 10x: il modulo richiede 10 volte più risorse degli altri
- Frequenza di deploy: il modulo necessità di rilasci 5x più frequenti
- Latenza SLA: il modulo ha SLA di latenza diversi (es. < 10ms per API critica)
- Dimensione del team: più di 5 sviluppatori lavorano esclusivamente sul modulo
Criteri Qualitativi
- Stack tecnologico: il modulo beneficerebbe di un linguaggio o runtime diverso
- Isolamento dei fallimenti: un bug nel modulo può causare il crash dell'intero sistema
- Compliance: il modulo gestisce dati con requisiti di sicurezza o compliance specifici
- Caching: il modulo ha pattern di caching molto diversi dal resto
Regola Pratica per l'Estrazione
Non estrarre un modulo come microservizio finchè non hai almeno 3 criteri quantitativi soddisfatti. L'estrazione prematura introduce complessità distribuita senza benefici proporzionali. Ricorda: il modular monolith e progettato per rendere l'estrazione facile quando serve, non per forzarla.
Configurazione Kubernetes Completa
Ecco una configurazione Kubernetes di riferimento per il deployment di un modular monolith in produzione, con health checks, resource limits, e affinity rules:
# Deployment completo per modular monolith
apiVersion: apps/v1
kind: Deployment
metadata:
name: ecommerce-app
labels:
app: ecommerce
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: ecommerce
template:
metadata:
labels:
app: ecommerce
spec:
containers:
- name: app
image: registry.example.com/ecommerce-app:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "production"
- name: JAVA_OPTS
value: "-Xms512m -Xmx2g -XX:+UseG1GC"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: ecommerce
Monitoraggio e Osservabilità
Un vantaggio significativo del modular monolith e la semplicità del monitoraggio. Con un singolo processo, le metriche sono centralizzate, i log sono unificati, e il tracing e locale.
- Metriche: Spring Boot Actuator + Micrometer per esportare metriche a Prometheus
- Log: structured logging con correlation ID per tracciare i flussi tra moduli
- Health checks: endpoint per ogni modulo per verificare lo stato di salute individuale
- Dashboard: Grafana per visualizzare metriche aggregate e per modulo
Prossimo Articolo
Nel prossimo articolo affronteremo la migrazione step-by-step da un monolith legacy a un modular monolith: il pattern Strangler Fig, l'identificazione dei confini, l'estrazione fisica dei moduli, la migrazione degli eventi, e una timeline realistica con case study e metriche.







