← Semua picks

Migration Guide Recommended

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.

17 Juli 2026 · 13 menit ·Use case: Migrasi Java 8 ke 21 LTS dengan adoption Virtual Threads enterprise
Java 21Spring Boot 3Virtual ThreadsZGC

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:

  1. Java 8 end-of-life public support: Oracle JDK 8 paid support, OpenJDK 8 community-maintained tapi declining
  2. Security CVE accumulation: 24 CVE yang affect Java 8 di 2022-2024
  3. Performance opportunity: Virtual Threads + ZGC + Spring Boot 3 stack modern
  4. Hiring drift: engineer baru lulus universitas familiar Java 17/21, Java 8 makin scarce

Pricing + cost

Migration cost one-time

ItemCost
Tech lead 24 minggu full-timeRp 384 juta
4 engineer part-time (50%) 24 mingguRp 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
TotalRp 1,4 miliar

Saving post-migration

ItemSaving/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/bulanRp 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)

MetrikPre (Java 8 + G1GC)Post (Java 21 + Virtual Threads + ZGC)
Throughput max RPS per pod1.2502.620
p50 latency22ms14ms
p99 latency285ms185ms
p99.9 latency850ms420ms
GC pause p9945ms8ms
Memory RSS steady1,2GB1,1GB (similar, but more efficient utilization)
Pod count untuk handle peak84

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)

MetrikPre (Java 8)Post (Java 21 G1GC)
Throughput850 rec/s1.080 rec/s
p99 latency1,2 detik920ms

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:

  1. Branch dari main, upgrade Java + Spring Boot + library
  2. Fix compile error (javax → jakarta, deprecated API)
  3. Fix test (JUnit 5 migration, mocking library)
  4. Run regression test full
  5. Deploy ke staging, 14 hari paralel run dengan production Java 8
  6. Performance benchmark + GC analysis
  7. Adopt Virtual Threads selektif (untuk payment service)

Pilot lesson:

  • Spring Boot 2.7 → 3.x mandate jakarta.* namespace — semua import javax.persistencejakarta.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.

SprintService migratedCatatan
Sprint 1 (W8-9)4 service SimplePattern baseline
Sprint 2 (W10-11)4 service SimplePattern stabilized
Sprint 3 (W12-13)4 service MediumSpring Boot 3 challenge
Sprint 4 (W14-15)4 service MediumLibrary upgrade pain
Sprint 5 (W16-17)3 service ComplexCustom 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

  1. javax → jakarta namespace di test code. Easy miss test file karena CI fokus production code. OpenRewrite recipe org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta cover ~95% case, manual fix 5%.
  2. 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.
  3. 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.
  4. 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.
  5. 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

ItemJava 8 status quo (counterfactual)Java 21 migration + post
Compute 3 tahunRp 700 jutaRp 532 juta
Ops time JVM tuningRp 380 jutaRp 240 juta
Insiden GC / memoryRp 95 jutaRp 35 juta
Hiring premium Java 8 seniorRp 180 jutaRp 0
CVE mitigation effortRp 120 jutaRp 40 juta
Migration one-time costRp 0Rp 1,4 miliar
Total 3 tahunRp 1,475 miliarRp 2,25 miliar

Wait — counterfactual lebih murah cash? Yes, kalau hitung cash flow saja. Tapi:

  1. Java 8 EOL risk: kalau community support drop, CVE patching butuh in-house effort = Rp 200+ juta tambahan per CVE major.
  2. Hiring drag: Java 8 engineer makin scarce, salary premium 15-25% di 2026+.
  3. Vendor support contract Oracle JDK 8 paid: ~Rp 240 juta/tahun kalau pilih jalur ini (saya tidak quote di counterfactual).
  4. 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

// Pick Migration Guide lain


← Semua picks RSS feed