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.
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:
- Lead time deploy degradasi: dari 2 jam (2020) ke 12 jam (2024) — Conway’s law mismatch
- Incident frequency tinggi: 8 P0/tahun, MTTR rata-rata 4,5 jam
- Onboarding lambat: engineer baru 6-8 minggu vs target 3 minggu
- Module scaling impossible: billing module CPU-bound spike payday tapi seluruh monolith harus scale (waste)
- 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:
| Aspek | Modular monolith | Microservice langsung |
|---|---|---|
| Effort migrasi 240k LOC | 24-36 minggu | 60-90 minggu |
| Ops complexity post | 1 deploy unit | 7+ service + service mesh + observability + RPC framework |
| Distributed transaction | tidak perlu | wajib (saga pattern, outbox) |
| Operational overhead | low | high |
| Risk migration | sedang | tinggi |
| Team scale support | 25-50 dev OK | 50-200 dev |
| Cost engineering | Rp 1,8 miliar | Rp 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
| Item | Cost |
|---|---|
| Tech lead full-time 30 minggu | Rp 480 juta |
| 4 engineer part-time (50% allocation) 30 minggu | Rp 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 |
| Total | Rp 1,8 miliar |
Saving post-migration
| Item | Saving/bulan |
|---|---|
| Deploy frequency 4x increase, reduce incident remediation | Rp 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 remediation | Rp 18 juta |
| Faster feature delivery (revenue impact) | Rp 30 juta (estimate) |
| Total saving/bulan | Rp 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
- Customer (master data nasabah)
- Billing (tagihan + invoice)
- Payment (integration payment gateway)
- Meter Reading (data konsumsi)
- Notification (email, SMS dispatch)
- Audit (log compliance)
- 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:
- Move code ke dedicated package
com.bumn.app.notification - Define API publik via
@ApplicationModuleannotation - Setup Spring Modulith dependency verification test
- Refactor cross-module call jadi event-driven (ApplicationEventPublisher)
- 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:
| Sprint | Module | Effort | Catatan |
|---|---|---|---|
| Sprint 1 | Customer | 3 minggu | Master data, 100+ dependency, careful boundary |
| Sprint 2 | Billing | 4 minggu | Module terbesar, banyak business logic |
| Sprint 3 | Payment | 3 minggu | Integration external (BCA, Mandiri, OVO, GoPay) |
| Sprint 4 | Meter Reading | 2 minggu | Relatif self-contained |
| Sprint 5 | Reporting | 3 minggu | Banyak 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
| Metrik | Pre (monolith) | Post (modular monolith) |
|---|---|---|
| Lead time deploy p99 | 12 jam | 28 menit |
| Deploy frequency | 1-2/minggu | 8-12/minggu |
| P0 incident attributable to cross-module | 8/tahun | 1/tahun |
| MTTR P0 | 4,5 jam | 1,8 jam |
| Test execution time CI | 22 menit | 14 menit (parallel per module) |
| Onboarding engineer (produktif) | 6-8 minggu | 3 minggu |
| Throughput peak campaign | 1.200 TPS | 1.620 TPS (+35%) |
| Service availability | 99,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
- Module boundary salah. Boundary by technical layer (controller, service, repository) bukan by business domain. Refactor sesuai bounded context DDD.
- Event tanpa schema versioning. InvoiceCreatedEvent v1 schema change breaking subscriber. Setup schema versioning + compatibility check (similar to Avro / Protobuf di microservice).
- Shared DTO library balloon. Class shared semua di “shared” module → tetap coupling. Limit shared module ke true cross-cutting (Money, Tenant, AuditMetadata).
- Database schema cross-module foreign key. Foreign key billing.invoice → customer.customer_id = boundary leak. Pakai surrogate ID + lookup via service.
- 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
| Metrik | Pre | Post-cutover 6 bulan |
|---|---|---|
| Cost compute/bulan | Rp 18 juta | Rp 22 juta (sedikit naik karena observability) |
| Cost ops engineer | Rp 22 juta | Rp 14 juta (less firefighting) |
| Lead time deploy | 12 jam | 28 menit |
| Engineer satisfaction (survey 1-10) | 5,8 | 8,3 |
| Feature velocity (story point/sprint) | 35-45 | 65-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