← Semua picks

Migration Guide Recommended

Migrasi Monolith → Modular Monolith Spring

Migrasi monolith Spring Boot 240k LOC ke modular monolith di BUMN energy Jakarta, 30 minggu kerja. Throughput +35%, lead time deploy turun dari 12 jam ke 28 menit. Pattern Spring Modulith. Verdict Recommended untuk enterprise yang belum siap microservice.

10 Juli 2026 · 13 menit ·Use case: Refactor monolith Spring enterprise jadi modular monolith
Spring Boot 3Java 21Spring Modulith

TL;DR

  • Scope migrasi: monolith Spring Boot 240k LOC, 38 domain entity, 7 module akhir di BUMN energy Jakarta.
  • Timeline: 30 minggu kalender, 4 engineer part-time + 1 tech lead full-time.
  • Outcome: throughput +35%, lead time deploy dari 12 jam ke 28 menit, P0 incident attributable to monolith complexity dari 8/tahun ke 1/tahun.
  • Cost: Rp 1,8 miliar one-time engineering. Saving Rp 65 juta/bulan ongoing (faster deploy + less incident).
  • Verdict: Recommended untuk enterprise dengan monolith mature tapi belum siap microservice operational.

Konteks

Saya pimpin migrasi ini di BUMN energy Jakarta, periode Oktober 2024 - Mei 2025 (30 minggu). Konteks bisnis:

  • Aplikasi core: customer service + billing + meter reading + payment integration
  • Monolith Spring Boot 2.7 (kemudian 3.2), Java 11 (kemudian 21 LTS), Oracle DB 19c, deploy ke VM on-prem 4 instance load-balanced
  • 240k LOC, 38 domain entity, 22 contributor tim engineering aktif
  • 6 tahun age, semula greenfield 2018 dengan domain layer baik tapi accumulate technical debt 2020-2024

Pain point sebelum migrasi:

  • Lead time deploy: 8-12 jam (build 1,5 jam + regression test 3 jam + manual smoke 2 jam + approval window + deploy 1 jam)
  • Deploy frequency: 1-2x per minggu (sebelumnya per hari, terdegradasi karena risk)
  • P0 incident: 8/tahun dengan root cause “cross-module change accidentally breaking unrelated feature”
  • Onboarding engineer baru: 6-8 minggu sebelum produktif
  • Hot module (billing) scaling: tidak bisa, terikat ke deployment unit monolithic

Driver migrasi

5 alasan formal yang masuk proposal:

  1. Lead time deploy degradasi: dari 2 jam (2020) ke 12 jam (2024) — Conway’s law mismatch
  2. Incident frequency tinggi: 8 P0/tahun, MTTR rata-rata 4,5 jam
  3. Onboarding lambat: engineer baru 6-8 minggu vs target 3 minggu
  4. Module scaling impossible: billing module CPU-bound spike payday tapi seluruh monolith harus scale (waste)
  5. Roadmap menuju microservice di 3-5 tahun: butuh stepping stone, jangan big-bang refactor

Pilihan pattern: kenapa modular monolith, bukan microservice langsung

Kalkulasi cost-benefit:

AspekModular monolithMicroservice langsung
Effort migrasi 240k LOC24-36 minggu60-90 minggu
Ops complexity post1 deploy unit7+ service + service mesh + observability + RPC framework
Distributed transactiontidak perluwajib (saga pattern, outbox)
Operational overheadlowhigh
Risk migrationsedangtinggi
Team scale support25-50 dev OK50-200 dev
Cost engineeringRp 1,8 miliarRp 5-8 miliar

Untuk BUMN 22 engineer + traffic predictable: modular monolith sweet spot. Plan microservice extraction kalau team scale 60+ atau hot module butuh independent SLO.

Pricing + cost

Migration cost one-time

ItemCost
Tech lead full-time 30 mingguRp 480 juta
4 engineer part-time (50% allocation) 30 mingguRp 800 juta
Architecture review (eksternal consultant 8 minggu)Rp 240 juta
Tooling + Spring Modulith license (gratis)Rp 0
Training tim (Spring Modulith + DDD workshop 3 hari)Rp 75 juta
Test infrastructure (parallel CI + observability)Rp 120 juta
Contingency 5%Rp 90 juta
TotalRp 1,8 miliar

Saving post-migration

ItemSaving/bulan
Deploy frequency 4x increase, reduce incident remediationRp 25 juta
Onboarding engineer 60% faster (2 hire/tahun savings)Rp 12 juta
Hot module independent scale (CPU optimization)Rp 8 juta
Reduce P0 incident remediationRp 18 juta
Faster feature delivery (revenue impact)Rp 30 juta (estimate)
Total saving/bulanRp 93 juta

ROI break-even: bulan ke-20. Net positive tahun ke-2 onwards.

Plan + timeline aktual

Minggu 1-4: Domain discovery + module boundary design

Activities:

  • Event Storming workshop 2 hari dengan tim engineering + product
  • Code archaeology: scan dependency graph monolith (jdeps + ArchUnit)
  • Identify domain context (bounded context DDD)
  • Draft module boundary

Output: 7 module proposed

  1. Customer (master data nasabah)
  2. Billing (tagihan + invoice)
  3. Payment (integration payment gateway)
  4. Meter Reading (data konsumsi)
  5. Notification (email, SMS dispatch)
  6. Audit (log compliance)
  7. Reporting (BI + regulatory report)

Minggu 5-10: Pilot extract 2 module

Pilih 2 module pilot:

  • Notification (low coupling, low risk)
  • Audit (cross-cutting, gampang isolate)

Per module:

  1. Move code ke dedicated package com.bumn.app.notification
  2. Define API publik via @ApplicationModule annotation
  3. Setup Spring Modulith dependency verification test
  4. Refactor cross-module call jadi event-driven (ApplicationEventPublisher)
  5. Database table boundary: notification table tinggal di shared DB tapi access via repository module-scoped

Pilot lesson:

  • Spring Modulith dependency verification powerful tapi butuh tuning rule (allow shared DTO library)
  • Event-driven cross-module communication add latency ~5-15ms (acceptable untuk non-critical path)
  • Database refactor (schema separation) terlalu mahal untuk pilot — kita keep shared DB

Minggu 11-22: Rollout 5 module sisa

Per module 2-3 minggu effort:

SprintModuleEffortCatatan
Sprint 1Customer3 mingguMaster data, 100+ dependency, careful boundary
Sprint 2Billing4 mingguModule terbesar, banyak business logic
Sprint 3Payment3 mingguIntegration external (BCA, Mandiri, OVO, GoPay)
Sprint 4Meter Reading2 mingguRelatif self-contained
Sprint 5Reporting3 mingguBanyak query cross-domain, butuh care boundary

Strategy: tiap sprint extract 1 module dengan dual-run validation (old code path + new modular path), traffic shift gradual 7-14 hari.

Minggu 23-28: Hardening + observability

  • Setup metric per module (Micrometer + Prometheus tags per module)
  • Distributed tracing antar module via OpenTelemetry (walaupun monolith, observability per-module tetap valuable)
  • Performance benchmark per module
  • Load test campaign payday scenario

Minggu 29-30: Retrospective + handover

  • Documentation: module boundary + API publik + event catalog
  • Knowledge transfer ke tim engineering full
  • Spring Modulith documentation generator output (auto-generated diagram)
  • Retrospective lesson learned

Architecture pattern

Module structure

com.bumn.app/
├── billing/                    # Module: Billing
│   ├── api/                    # API publik
│   │   ├── BillingService.java
│   │   └── BillingDTO.java
│   ├── domain/                 # Internal domain
│   │   ├── Invoice.java
│   │   ├── BillingCycle.java
│   │   └── BillingPolicy.java
│   ├── infrastructure/         # DB, external
│   │   └── InvoiceRepository.java
│   └── package-info.java       # @ApplicationModule annotation

├── payment/                    # Module: Payment
│   ├── api/
│   ├── domain/
│   └── infrastructure/

├── customer/                   # Module: Customer
└── shared/                     # Shared DTO + utility (carefully scoped)

package-info.java:

@org.springframework.modulith.ApplicationModule(
    displayName = "Billing",
    allowedDependencies = {"customer", "shared"}
)
package com.bumn.app.billing;

ArchUnit + Spring Modulith verify: code di billing/ tidak boleh access payment/ directly. Cross-module communication via event publishing atau service API publik.

Event-driven cross-module communication

// Billing module publish event
@Service
public class BillingService {
    private final ApplicationEventPublisher events;
    
    public void createInvoice(InvoiceData data) {
        Invoice invoice = ... // domain logic
        events.publishEvent(new InvoiceCreatedEvent(invoice.getId(), customer.getId(), amount));
    }
}

// Notification module subscribe
@Component
public class InvoiceNotificationListener {
    @ApplicationModuleListener
    void onInvoiceCreated(InvoiceCreatedEvent event) {
        // send email / SMS
    }
}

@ApplicationModuleListener (Spring Modulith) = @TransactionalEventListener(after=COMMIT) + @Async — event published setelah transaction commit, processed async.

Trade-off: eventual consistency. Untuk use case yang strict consistent (payment confirmation), tetap pakai service call direct via API publik.

Database boundary (incremental)

Phase 1 (selesai bulan ke-22): shared DB tapi schema per module logical separation. Setiap module hanya akses table-nya sendiri via repository.

Phase 2 (planned 12 bulan post-migration): physical schema separation per module. Cross-schema query forbidden. Foreign key cross-schema = surrogate ID lookup.

Phase 3 (kalau microservice extraction): separate DB per service.

SLO post-migration

MetrikPre (monolith)Post (modular monolith)
Lead time deploy p9912 jam28 menit
Deploy frequency1-2/minggu8-12/minggu
P0 incident attributable to cross-module8/tahun1/tahun
MTTR P04,5 jam1,8 jam
Test execution time CI22 menit14 menit (parallel per module)
Onboarding engineer (produktif)6-8 minggu3 minggu
Throughput peak campaign1.200 TPS1.620 TPS (+35%)
Service availability99,82%99,94%

Trade-off

Pro modular monolith vs full microservice

  • Operational simplicity (1 deploy unit vs multi-service)
  • No distributed transaction complexity
  • Single observability stack (per-module via tags, not separate stack)
  • Reasonable migration effort (6-8 bulan vs 1,5-2 tahun)
  • Team scale support sampai ~50 engineer

Con modular monolith vs full microservice

  • Tidak bisa scale module independent (semua scale together)
  • Tidak bisa pick bahasa berbeda per module (semua Spring/Java)
  • Build time tetap monolith (lebih cepat dari sebelum, tapi tidak per-service speed)
  • Team autonomy lebih rendah dari microservice

Pro modular monolith vs monolith biasa

  • Boundary enforced via tooling
  • Refactor safe (compile-time + test-time verification)
  • Setup foundation untuk future microservice extraction

Con modular monolith vs monolith biasa

  • Effort migrasi 6-8 bulan
  • Discipline tim harus tinggi (jangan revert ke spaghetti)
  • Annotation + tooling overhead (Spring Modulith)

Common pitfall

  1. Module boundary salah. Boundary by technical layer (controller, service, repository) bukan by business domain. Refactor sesuai bounded context DDD.
  2. Event tanpa schema versioning. InvoiceCreatedEvent v1 schema change breaking subscriber. Setup schema versioning + compatibility check (similar to Avro / Protobuf di microservice).
  3. Shared DTO library balloon. Class shared semua di “shared” module → tetap coupling. Limit shared module ke true cross-cutting (Money, Tenant, AuditMetadata).
  4. Database schema cross-module foreign key. Foreign key billing.invoice → customer.customer_id = boundary leak. Pakai surrogate ID + lookup via service.
  5. Tidak setup verification di CI. Tanpa Spring Modulith verification di CI, boundary drift dalam 6 bulan. Wajib fail build kalau violation.

Hiring + training

Pre-migration: tim 22 engineer, 80% familiar Spring tapi 0 familiar Spring Modulith (baru GA 2024).

Training plan:

  • Workshop DDD + Bounded Context (2 hari, eksternal consultant)
  • Workshop Spring Modulith (1 hari, internal lead presentation)
  • Pair programming dengan tech lead untuk first 2 module
  • Code review intensive (2 reviewer mandatory untuk semua refactor PR)

Hiring tambahan: 1 senior architect 6-bulan contract untuk Q1-Q2 2025 (Rp 240 juta). Worth-nya: review architecture decisions weekly + escalation untuk konflik tim.

Common pitfall part 2: real war story

War story 1: Bulan ke-6, kami refactor Billing module. Engineer junior accidentally remove transitive dependency yang dipakai Reporting. Build pass (Spring Modulith verification lulus karena tidak ada cross-module call), tapi runtime fail (NullPointerException di Reporting saat akses billing data via deprecated bridge code).

Fix: tambah integration test cross-module di CI yang exercise full flow, bukan hanya unit test per module.

War story 2: Bulan ke-15, audit OJK pertama pasca-migrasi. Auditor minta jelaskan boundary module + akses control. Auto-generated documentation Spring Modulith (PlantUML diagram + event catalog) jadi saver. Auditor express satisfaction dengan dokumentasi formal.

War story 3: Bulan ke-22, hot module (Billing) butuh JVM heap 2x lipat untuk handle campaign payday. Karena masih 1 deploy unit, harus scale seluruh monolith. Cost compute 2x dari sebelum migrasi. Plan microservice extraction Billing module mulai dibahas (target 2026-2027).

Outcome 6 bulan post-cutover

MetrikPrePost-cutover 6 bulan
Cost compute/bulanRp 18 jutaRp 22 juta (sedikit naik karena observability)
Cost ops engineerRp 22 jutaRp 14 juta (less firefighting)
Lead time deploy12 jam28 menit
Engineer satisfaction (survey 1-10)5,88,3
Feature velocity (story point/sprint)35-4565-80

Saving operational + velocity gain = Rp 65-95 juta/bulan value. ROI break-even bulan ke-20, net positive bulan ke-24 onwards.

Yang surprising

Setelah 30 minggu: discipline tim untuk maintain boundary jadi pain point bigger dari technical. 4 violation muncul di 6 bulan pertama post-migration (engineer revert ke pattern lama karena “lebih cepat dalam jangka pendek”). Setiap violation jadi pelajaran formal review meeting.

Surprise lain: event-driven cross-module communication ternyata mengubah cara tim think tentang domain. Engineer mulai mendiskusikan “event apa yang muncul saat invoice created” daripada “siapa yang panggil siapa”. Pattern thinking matang ini jadi foundation untuk eventual microservice extraction (kalau pun terjadi).

Verdict

Recommended untuk enterprise Indonesia dengan monolith mature.

  • Eligible: monolith Spring Boot (atau Java enterprise lain) 100k+ LOC, tim engineering 15-50 dev, ada pain point lead time + cross-module incident.
  • Pre-requisite: Java 21 LTS + Spring Boot 3.x, 24-36 minggu kalender budget, tech lead full-time + 3-4 engineer part-time, Rp 1,5-2,5 miliar one-time.
  • Tidak eligible: tim < 10 dev (overhead tidak justify), monolith < 50k LOC (refactor regular cukup), traffic predictable stable tanpa scale issue.
  • Skip kalau Anda sudah commit ke microservice — modular monolith bukan endpoint, microservice langsung kalau ops capacity siap.

Threshold konkret untuk start migration: lead time deploy > 4 jam + cross-module incident frequency > 4/tahun + tim engineering > 15. Di bawah ini, refactor regular per-domain cukup.

Ditulis oleh Asti Larasati

// Pick Migration Guide lain


← Semua picks RSS feed