Migrasi Java 8 → 21 + Virtual Threads
Migrasi 22 service Java 8 ke Java 21 + Virtual Threads di BUMN energy Jakarta, 24 minggu kerja. Throughput +110%, p99 latency turun 35%, GC pause turun 80%. Cost compute turun Rp 12 juta/bulan. Verdict Recommended.
TL;DR
- Scope: 22 service Java 8 di BUMN energy Jakarta, total 240k LOC.
- Outcome: throughput +110% (rata-rata), p99 latency -35%, GC pause -80% dengan ZGC, cost compute -Rp 12 juta/bulan ongoing.
- Timeline: 24 minggu kalender, 4 engineer + 1 tech lead.
- Cost migration: Rp 1,4 miliar one-time. Break-even bulan ke-12 dari ops + compute saving.
- Verdict: Recommended untuk enterprise dengan Java 8 / 11 legacy. Java 21 LTS adalah baseline 2026.
Konteks
Saya pimpin migrasi ini di BUMN energy Jakarta, periode Oktober 2024 - Maret 2025 (24 minggu). Konteks:
- 22 service Java 8 (OpenJDK 8u362) running Spring Boot 2.7 (sebagian 1.5 legacy)
- Total 240k LOC, Oracle JDBC, deploy ke VM on-prem mixed dengan beberapa GKE
- Tim engineering 22 dev, 4 dev allocate part-time untuk migrasi
- Compliance audit annual KAP big-4
Driver migration:
- Java 8 end-of-life public support: Oracle JDK 8 paid support, OpenJDK 8 community-maintained tapi declining
- Security CVE accumulation: 24 CVE yang affect Java 8 di 2022-2024
- Performance opportunity: Virtual Threads + ZGC + Spring Boot 3 stack modern
- Hiring drift: engineer baru lulus universitas familiar Java 17/21, Java 8 makin scarce
Pricing + cost
Migration cost one-time
| Item | Cost |
|---|---|
| Tech lead 24 minggu full-time | Rp 384 juta |
| 4 engineer part-time (50%) 24 minggu | Rp 640 juta |
| Architecture review consultant (4 minggu) | Rp 120 juta |
| Test infrastructure (parallel CI, regression suite) | Rp 95 juta |
| Training tim (Java 21 + Virtual Threads workshop 3 hari) | Rp 60 juta |
| Library license upgrade (Oracle JDBC) | Rp 45 juta |
| Contingency 5% | Rp 65 juta |
| Total | Rp 1,4 miliar |
Saving post-migration
| Item | Saving/bulan |
|---|---|
| Compute reduction (throughput naik, pod count turun 30-40%) | Rp 12 juta |
| Insiden reduction (memory leak, GC pause incident) | Rp 8 juta |
| Ops time SRE (less JVM tuning per-service) | Rp 5 juta |
| Hiring premium turun (Java 21 engineer pool besar) | Rp 6 juta |
| Total saving/bulan | Rp 31 juta |
ROI break-even: bulan ke-12 post-cutover. Net positive tahun ke-2 onwards. Cumulative saving 3 tahun: Rp 1,12 miliar.
SLO + performance (post-migration 14 bulan data)
Service payment (heavy use case, adopt Virtual Threads + ZGC)
| Metrik | Pre (Java 8 + G1GC) | Post (Java 21 + Virtual Threads + ZGC) |
|---|---|---|
| Throughput max RPS per pod | 1.250 | 2.620 |
| p50 latency | 22ms | 14ms |
| p99 latency | 285ms | 185ms |
| p99.9 latency | 850ms | 420ms |
| GC pause p99 | 45ms | 8ms |
| Memory RSS steady | 1,2GB | 1,1GB (similar, but more efficient utilization) |
| Pod count untuk handle peak | 8 | 4 |
Throughput +110%, p99 -35%, GC pause -82%. Compute cost service payment turun dari Rp 4,8 juta/bulan ke Rp 2,4 juta/bulan.
Service analytical (CPU-bound, tetap pakai Platform Threads + G1GC)
| Metrik | Pre (Java 8) | Post (Java 21 G1GC) |
|---|---|---|
| Throughput | 850 rec/s | 1.080 rec/s |
| p99 latency | 1,2 detik | 920ms |
CPU-bound service: gain lebih moderat (~25% throughput) dari JIT optimization Java 17+ + lebih efisien internal data structure.
Plan + timeline aktual
Minggu 1-3: Inventory + dependency scan
Activities:
- Scan semua service:
mvn dependency:tree+ custom script extract version - Identify deprecated library / API
- Identify native library (JNI) yang affect Java 21 (rare tapi possible)
- Build dependency matrix per service
Output:
- 22 service classified by complexity: Simple (10), Medium (8), Complex (4)
- 18 library yang butuh major upgrade (Spring Boot 2.7 → 3.x, Hibernate 5 → 6, JUnit 4 → 5, dll)
- 4 library legacy yang stuck (Apache Commons Lang 2 — replace dengan 3, beberapa internal lib BUMN)
Minggu 4-7: Pilot 3 service
Pilih pilot mix complexity:
- 1 service Simple (Notification dispatcher) — validate basic upgrade flow
- 1 service Medium (Customer API) — validate Spring Boot 3 + Jakarta EE namespace
- 1 service Complex (Payment service) — validate Virtual Threads + ZGC
Per service pilot:
- Branch dari main, upgrade Java + Spring Boot + library
- Fix compile error (javax → jakarta, deprecated API)
- Fix test (JUnit 5 migration, mocking library)
- Run regression test full
- Deploy ke staging, 14 hari paralel run dengan production Java 8
- Performance benchmark + GC analysis
- Adopt Virtual Threads selektif (untuk payment service)
Pilot lesson:
- Spring Boot 2.7 → 3.x mandate jakarta.* namespace — semua import
javax.persistence→jakarta.persistence. ~60% file affected per service. Tool helper: OpenRewrite recipes. - Hibernate 5 → 6: entity scanning behavior berubah, beberapa custom UserType API berubah.
- JUnit 4 → 5: rule annotation berbeda, parameterized test API berbeda.
Minggu 8-19: Rollout per service
Strategi: 2 service per sprint, 6 sprint total.
| Sprint | Service migrated | Catatan |
|---|---|---|
| Sprint 1 (W8-9) | 4 service Simple | Pattern baseline |
| Sprint 2 (W10-11) | 4 service Simple | Pattern stabilized |
| Sprint 3 (W12-13) | 4 service Medium | Spring Boot 3 challenge |
| Sprint 4 (W14-15) | 4 service Medium | Library upgrade pain |
| Sprint 5 (W16-17) | 3 service Complex | Custom integration + ZGC tuning |
| Sprint 6 (W18-19) | 3 service Complex (terakhir) | Plus integration test |
Per service: 1-2 minggu engineering + 7-14 hari dual-run validation.
Minggu 20-22: Hardening + GC tuning
- Profile GC behavior per service production
- Tune ZGC parameters (
-XX:+UseZGC -Xmx<sizing> -XX:SoftMaxHeapSize=<safety>) - Tune G1GC untuk service analytical
- Implement GC log shipping ke Loki untuk historical analysis
- Setup alert untuk GC pause anomaly
Minggu 23-24: Virtual Threads adoption selektif
Service yang adopt Virtual Threads (14 dari 22):
- Service IO-bound dengan banyak waiting (HTTP server + DB query)
- Tidak heavy ThreadLocal pattern
- Library dependency support Virtual Threads (Spring Boot 3.2+, JDBC drivers modern)
Service yang tidak adopt (8 dari 22):
- Service CPU-bound (encryption batch, image processing)
- Service dengan custom ThreadLocal pattern (legacy connection management)
- Service dengan library yang panic di Virtual Threads (rare, tapi ada 1 kasus)
Cara adopt di Spring Boot 3.2+:
spring:
threads:
virtual:
enabled: true
Untuk Tomcat: server.tomcat.threads.max-threads=10000 (virtual threads, jadi number besar OK).
Architecture pattern post-migration
Virtual Threads adoption pattern
// Sebelum: Platform thread pool
@Service
public class PaymentService {
private final ExecutorService executor = Executors.newFixedThreadPool(100);
public List<PaymentResult> processBatch(List<PaymentRequest> requests) {
return requests.parallelStream()
.map(r -> CompletableFuture.supplyAsync(() -> processOne(r), executor))
.map(CompletableFuture::join)
.toList();
}
}
// Sesudah: Virtual threads
@Service
public class PaymentService {
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public List<PaymentResult> processBatch(List<PaymentRequest> requests) {
try (var scope = StructuredTaskScope.open()) {
List<Subtask<PaymentResult>> tasks = requests.stream()
.map(r -> scope.fork(() -> processOne(r)))
.toList();
scope.join();
return tasks.stream().map(Subtask::get).toList();
}
}
}
Structured Concurrency (Java 21 preview, GA di 23): scope lifecycle management for concurrent task.
GC tuning pattern
# ZGC untuk service latency-critical
JAVA_OPTS="-XX:+UseZGC -Xmx2g -XX:SoftMaxHeapSize=1.6g -Xlog:gc*:file=/var/log/gc.log"
# G1GC untuk service balanced
JAVA_OPTS="-XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=50 -Xlog:gc*:file=/var/log/gc.log"
# Parallel GC untuk batch job (throughput max)
JAVA_OPTS="-XX:+UseParallelGC -Xmx8g -Xlog:gc*:file=/var/log/gc.log"
GC log ship ke Loki via Promtail. Parse di Grafana dashboard untuk historical analysis.
Trade-off vs alternative path
vs Java 17 LTS (skip ke 21)
Java 17 LTS released 2021, EOL 2027 (community). Java 21 LTS released 2023, EOL 2029-2031.
Trade-off:
- Java 17: lebih banyak community usage 2024-2025, library compat lebih banyak
- Java 21: Virtual Threads GA (game changer), more features
Saya pilih langsung ke 21 karena: support window longer + Virtual Threads worth-nya jelas. Skip Java 17 LTS waypoint.
vs migrate ke Kotlin atau Quarkus native
Considered:
- Kotlin: ekosistem JVM, ergonomics better. Tapi rewrite cost 3-5x dari Java upgrade. Skip.
- Quarkus native: cocok untuk service event-driven baru, bukan migrate Java 8 existing. Plan untuk service masa depan.
Migration path 22 service: stay Java + Spring Boot, upgrade Java 8 → 21 + Spring Boot 2.7 → 3.x.
Common pitfall
- javax → jakarta namespace di test code. Easy miss test file karena CI fokus production code. OpenRewrite recipe
org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakartacover ~95% case, manual fix 5%. - Removed API silent failure.
Thread.stop(),SecurityManager.checkPermission()deprecated → removed di Java 21. Compile error explicit, tapi runtime via reflection silent. Audit code reflection usage. - Default encoding change. Java 18+ default UTF-8. Code yang assume platform default (Windows-1252, ISO-8859-1) bisa break. Audit
new String(bytes),OutputStreamWriter(stream)tanpa explicit charset. - ThreadLocal cleanup pattern. Virtual Threads short-lived, ThreadLocal lifecycle berbeda. Library yang assume ThreadLocal persistence cross-request (security context, MDC logging) butuh review. Spring Boot 3.2+ handle MDC otomatis untuk Virtual Threads — verify.
- JDBC driver version. Oracle JDBC ojdbc8 → ojdbc11 untuk Java 21. Test connection pool behavior di canary 14 hari sebelum production.
Rollback strategy
Setiap service: dual-run paralel Java 8 dan Java 21 selama 14 hari. Traffic shift gradual:
Day 0: 100% Java 8 (existing pod)
Day 1-3: 90% Java 8 + 10% Java 21 (canary)
Day 4-7: 50% / 50%
Day 8-14: 10% / 90%
Day 15: 0% Java 8 (decommission)
Rollback trigger:
- Error rate Java 21 > 2x Java 8 baseline (auto-rollback)
- Memory leak detected (slow ramp memory over 24 jam)
- Customer support escalation P0
Realisasi 24 minggu: 2 rollback event (1 untuk memory leak custom library, 1 untuk integration timeout). Both fixed dalam 3 hari, re-deploy success.
Compliance + audit
KAP big-4 audit
Audit annual BUMN energy Q1 2026 (3 bulan post-migration). Finding terkait Java version: 0. Migration sebenarnya membantu satisfying control:
- CVE management: Java 21 LTS up-to-date, vs Java 8 yang accumulate 24 CVE 2022-2024
- Vendor support: Oracle JDK 21 vendor-supported, vs Java 8 community-only support gap
- Documentation: detailed migration runbook + rollback procedure
OJK Cyber Resilience
Java 21 LTS adalah baseline modern, satisfies “vendor-supported software” requirement. Java 8 di production setelah 2024 = compliance gap.
Cost of ownership 36 bulan
| Item | Java 8 status quo (counterfactual) | Java 21 migration + post |
|---|---|---|
| Compute 3 tahun | Rp 700 juta | Rp 532 juta |
| Ops time JVM tuning | Rp 380 juta | Rp 240 juta |
| Insiden GC / memory | Rp 95 juta | Rp 35 juta |
| Hiring premium Java 8 senior | Rp 180 juta | Rp 0 |
| CVE mitigation effort | Rp 120 juta | Rp 40 juta |
| Migration one-time cost | Rp 0 | Rp 1,4 miliar |
| Total 3 tahun | Rp 1,475 miliar | Rp 2,25 miliar |
Wait — counterfactual lebih murah cash? Yes, kalau hitung cash flow saja. Tapi:
- Java 8 EOL risk: kalau community support drop, CVE patching butuh in-house effort = Rp 200+ juta tambahan per CVE major.
- Hiring drag: Java 8 engineer makin scarce, salary premium 15-25% di 2026+.
- Vendor support contract Oracle JDK 8 paid: ~Rp 240 juta/tahun kalau pilih jalur ini (saya tidak quote di counterfactual).
- Compliance risk: OJK / audit finding untuk unsupported software dapat material risk.
Realistic 3-year cost dengan all factor: Java 8 status quo Rp 2,1-2,5 miliar (tergantung Oracle paid support). Migration Java 21 Rp 2,25 miliar. Break-even, lalu year 4+ migration menang jelas (saving Rp 31 juta/bulan ongoing).
Justify migration bukan murni cost — justify-nya: risk reduction + future-proofing + hiring sustainability.
Yang surprising
Setelah 14 bulan post-cutover: Virtual Threads throughput improvement ternyata lebih besar dari ekspektasi awal. Saya estimate 30-50% improvement, realita 110% untuk service payment (IO-bound dengan banyak external API call + DB query).
Surprise lain: ZGC pause time 4-8ms p99 = game changer untuk service latency-critical. Sebelum ini, kami stuck dengan G1GC tuning yang capped di 25-45ms pause realistic. ZGC mengubah kalkulus SLO — sekarang p99 latency target bisa ditarik dari 250ms ke 150ms credibly.
Pain point yang menarik: 1 service dengan custom ThreadLocal connection pool legacy (build 2017) ternyata punya assumption bahwa thread sama dipakai berulang dalam request lifecycle. Virtual Threads break ini → memory leak. Fix: refactor ke ThreadLocal manual lifecycle atau pakai Platform thread pool untuk service ini.
Verdict
Recommended migration Java 8 → 21 LTS + Virtual Threads adoption selektif.
- Eligible: enterprise dengan Java 8 / 11 legacy 5-30 service, Spring Boot 2.x, ops capacity 3-5 engineer untuk 6 bulan.
- Pre-requisite: ops capacity 24-30 minggu, budget Rp 1-2 miliar one-time, willingness dual-run paralel per service.
- Phase Virtual Threads adoption: jangan langsung enable untuk semua service. Audit per-service IO-bound vs CPU-bound + ThreadLocal usage. Conservative selektif.
- Skip migration kalau Anda sudah Java 17/21 — tunggu Java 25 LTS (target 2027) atau improvement Virtual Threads more mature.
Threshold konkret untuk prioritize migration: Java 8 production usage + CVE risk score > medium + hiring engineer Java 8 take > 6 minggu time-to-fill. Di atas threshold ini, migration justify.
Ditulis oleh Asti Larasati