Repository navigation
Expand file tree
/
Copy pathExpTwelveFactor.java
More file actions
174 lines (144 loc) · 9.63 KB
/
Copy pathExpTwelveFactor.java
File metadata and controls
174 lines (144 loc) · 9.63 KB
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
import java.util.Map;
/**
* The Twelve-Factor App aplicado a Java/Spring.
* Cada factor se ilustra con código o explicación ejecutable.
*
* Referencia: https://12factor.net
*/
public class ExpTwelveFactor {
// ═══════════════════════════════════════════════════════════════
// FACTOR I: CODEBASE — un repo, múltiples deploys
// ═══════════════════════════════════════════════════════════════
// Un solo repositorio Git. Las diferencias entre staging y producción
// se gestionan con variables de entorno, no con ramas ni forks.
//
// Bien: git@github.com:empresa/mi-servicio.git → deploy staging, producción, QA
// Mal: un repo por entorno (mi-servicio-prod, mi-servicio-staging)
static void factorI() {
System.out.println("Factor I — Codebase: un repo, entornos separados por config");
System.out.println(" → spring.profiles.active=prod|staging|dev");
}
// ═══════════════════════════════════════════════════════════════
// FACTOR III: CONFIG — env vars, nunca hardcoded
// ═══════════════════════════════════════════════════════════════
static void factorIII() {
System.out.println("\nFactor III — Config:");
// MAL: valor hardcodeado que varía entre entornos
String dbUrlBad = "jdbc:postgresql://localhost:5432/mydb";
// BIEN: viene del entorno; en Spring Boot sería @Value("${DB_URL}")
String dbUrl = System.getenv("DB_URL");
if (dbUrl == null) dbUrl = "jdbc:postgresql://localhost:5432/mydb"; // fallback local
System.out.println(" DB_URL (env): " + dbUrl);
// La config que debe viajar por env vars:
// - credenciales de BD y servicios externos
// - puertos y hosts
// - feature flags de infraestructura
// Lo que NO va por env var: config interna de la app (timeouts de reintentos, etc.)
}
// ═══════════════════════════════════════════════════════════════
// FACTOR IV: BACKING SERVICES — DB, queues, cache como recursos adjuntos
// ═══════════════════════════════════════════════════════════════
// Un backing service es cualquier servicio que la app consume por red.
// No debe haber distinción en el código entre local y externo — solo cambia la URL.
interface MessageQueue {
void publish(String topic, String message);
}
static class KafkaQueue implements MessageQueue {
private final String brokers;
KafkaQueue(String brokers) { this.brokers = brokers; }
public void publish(String topic, String message) {
System.out.println(" [Kafka@" + brokers + "] topic=" + topic + " msg=" + message);
}
}
static void factorIV() {
System.out.println("\nFactor IV — Backing Services como recursos adjuntos:");
String brokers = System.getenv("KAFKA_BROKERS");
if (brokers == null) brokers = "localhost:9092";
MessageQueue queue = new KafkaQueue(brokers);
queue.publish("orders", "order-created");
// Cambiar de Kafka local a Kafka gestionado = solo cambiar KAFKA_BROKERS
}
// ═══════════════════════════════════════════════════════════════
// FACTOR VI: PROCESSES — stateless, sin estado compartido entre requests
// ═══════════════════════════════════════════════════════════════
// MAL: estado de sesión en memoria (no sobrevive a reinicios ni escala horizontal)
static class StatefulSessionBad {
private final Map<String, String> sessions = new java.util.HashMap<>();
void login(String userId, String token) { sessions.put(userId, token); }
boolean isAuthenticated(String userId) { return sessions.containsKey(userId); }
// Si hay 3 instancias, solo 1 tiene el token → falla en N-1 instancias
}
// BIEN: estado externalizado (Redis, JWT, BD)
static class StatelessRequestHandler {
// El token JWT viaja en cada request; no hay estado local
boolean isAuthenticated(String jwtToken) {
return jwtToken != null && jwtToken.startsWith("Bearer ");
}
}
static void factorVI() {
System.out.println("\nFactor VI — Processes stateless:");
StatelessRequestHandler handler = new StatelessRequestHandler();
System.out.println(" Auth con JWT: " + handler.isAuthenticated("Bearer eyJ..."));
System.out.println(" Auth sin token: " + handler.isAuthenticated(null));
}
// ═══════════════════════════════════════════════════════════════
// FACTOR IX: DISPOSABILITY — arranque rápido y shutdown graceful
// ═══════════════════════════════════════════════════════════════
static void factorIX() {
System.out.println("\nFactor IX — Disposability (shutdown hook):");
// En Spring Boot: @PreDestroy o SmartLifecycle
// En Java puro: Runtime shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println(" [SHUTDOWN] Cerrando conexiones y drenando colas...");
// 1. Dejar de aceptar nuevas peticiones
// 2. Esperar a que las peticiones en vuelo terminen (timeout configurable)
// 3. Cerrar conexiones a BD, Kafka, Redis
}, "shutdown-hook"));
System.out.println(" Shutdown hook registrado — el proceso puede terminar con seguridad");
}
// ═══════════════════════════════════════════════════════════════
// FACTOR XI: LOGS — tratar logs como event streams (stdout)
// ═══════════════════════════════════════════════════════════════
// La app no gestiona ficheros de log ni rotación. Solo escribe a stdout.
// La infraestructura (Docker, Kubernetes, Fluentd) captura y enruta los logs.
static void factorXI() {
System.out.println("\nFactor XI — Logs como event stream:");
// En Spring Boot: logging.level.root=INFO en application.properties
// y los logs van a stdout por defecto con Logback
// Simula una línea de log estructurado a stdout (como hace slf4j/logback)
long ts = System.currentTimeMillis();
System.out.printf(" {\"timestamp\":%d,\"level\":\"INFO\",\"msg\":\"Pedido procesado\",\"orderId\":\"ORD-1\"}%n", ts);
// MAL: escribir a fichero desde la app
// FileWriter fw = new FileWriter("/var/log/myapp.log", true);
// BIEN: System.out o un logger configurado para stdout
}
// ═══════════════════════════════════════════════════════════════
// FACTOR XII: ADMIN PROCESSES — migraciones como one-off tasks
// ═══════════════════════════════════════════════════════════════
// Las tareas administrativas (migraciones de BD, scripts de limpieza) se ejecutan
// como procesos one-off en el mismo entorno que la app, no como cron jobs separados.
// En Spring Boot: Flyway o Liquibase se ejecutan al arrancar la app.
static void factorXII() {
System.out.println("\nFactor XII — Admin processes:");
System.out.println(" Simulando migración one-off (equivalente a Flyway/Liquibase):");
runMigration("V1__create_orders_table.sql");
runMigration("V2__add_status_column.sql");
}
private static void runMigration(String scriptName) {
System.out.println(" [MIGRATION] Ejecutando: " + scriptName + " ... OK");
}
// ═══════════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════════
public static void main(String[] args) {
System.out.println("=== The Twelve-Factor App en Java ===\n");
factorI();
factorIII();
factorIV();
factorVI();
factorIX();
factorXI();
factorXII();
System.out.println("\n=== Fin demo 12-Factor ===");
}
}