karawaci.kode

2026-07-13 · 9 min

Legacy Modernization Tangerang ERP 3 Tahun

Tiga tahun lalu, klien manufaktur Tangerang (industri komponen otomotif, 6 pabrik, ~1.800 karyawan) datang dengan ERP legacy: PHP 5.6 + Smarty template + MySQL 5.7 + jQuery 1.7. Code base 2.1 juta LOC, modul 47 (inventory, production planning, HR, payroll, finance, …). Vendor lama bangkrut, tidak ada dokumentasi, satu engineer senior internal pegang knowledge — dia mau pensiun.

Tahun lalu, project selesai. Spring Boot 3 + Postgres 17, deployed bertahap selama 3 tahun, 0 big-bang downtime. Share methodology, decision penting, dan apa yang akan saya lakukan beda kalau ulang.

Setup awal (warisan)

  • Stack lama: PHP 5.6 (end-of-life 2018!), Smarty 2.x, MySQL 5.7, jQuery 1.7, Bootstrap 2.
  • Hosting: 2 server bare metal di colo Cibubur, no HA. Backup harian via rsync.
  • Codebase: 2.1M LOC PHP, 800K LOC JS, 47 modul logis, 1.840 tabel MySQL.
  • User: ~1.200 internal user concurrent, 24/7 (3 shift produksi).
  • Integrasi: 12 sistem external (perbankan, supplier portal, government e-faktur, …).

Pain point yang motivasi modernize:

  1. Security: PHP 5.6 EOL, banyak SQL injection legacy (string concat query).
  2. Compliance: audit ISO 27001 fail karena outdated stack.
  3. Knowledge bus factor 1: 1 engineer pensiun = sistem yatim piatu.
  4. Performance: query inventory reporting butuh 8-12 menit (tabel besar tanpa index proper).
  5. Mobile-ness zero: jQuery 1.7 + Bootstrap 2 = unusable di mobile.

Strategi: Strangler Fig pattern, NOT big-bang rewrite

Big-bang rewrite untuk ERP segini = death march. Tim manufaktur tidak bisa stop 18 bulan untuk rewrite. Plus knowledge bus factor: kalau rewrite gagal 12 bulan in, project abort, sunk cost massif.

Strangler Fig:

  • Bangun modern app di sebelahnya.
  • Pasang API gateway/proxy di depan legacy.
  • Route per-endpoint atau per-modul ke modern app saat siap.
  • Legacy “strangled” perlahan, modul demi modul.

Diagram conceptual:

[User] → [API Gateway/Reverse Proxy] → {
    /hr/* → Modern Spring Boot (year 1)
    /inventory/* → Modern Spring Boot (year 2)
    /production/* → Legacy PHP (year 3)
    /finance/* → Legacy PHP (year 3)
    /e-faktur/* → Modern Spring Boot (year 1)
    ...
}

Tiap modul migrate, route shift. Legacy tetap running sampai 0% traffic, baru di-decommission.

Year 1: foundation + edge modules (12 bulan)

Q1-Q2: tooling, observability, gateway

  • Setup Spring Boot 3 + Postgres 17 cluster baru di Hetzner colo Cibubur (same DC dengan legacy).
  • API Gateway: nginx + Lua scripting (OpenResty) untuk routing per-modul.
  • Authentication: SSO via Keycloak (federated dengan legacy session selama migration).
  • CI/CD: GitLab CI + ArgoCD untuk Kubernetes deploy (lihat juga Kubernetes cost cut Jakarta).
  • Observability: Prometheus + Grafana + Tempo (lihat OpenTelemetry microservices).

Q3: pilih modul edge untuk first migration

Kriteria modul edge:

  • Dependency masuk minimal (tidak banyak modul lain panggil).
  • Bisa direplikasi tanpa shared state besar dengan modul lain.
  • Critical enough untuk demonstrate value, simple enough untuk delivery.

Kandidat:

  • Document management (upload, search, attachment di workflow). 12 endpoint, satu tabel documents + S3-compat storage. Cocok.
  • E-faktur integration: government tax invoice API. Self-contained.

Saya pilih e-faktur dulu — pain point real (legacy struggle dengan e-faktur format baru DJP 2023).

Q4: e-faktur module migration

Endpoint count: 18. Tabel: 4 (einvoice, einvoice_items, einvoice_status, einvoice_log).

Data migration:

  • Dual-write: legacy + modern menulis bersamaan untuk 2 bulan.
  • Read shadow: tiap read, compare result legacy vs modern, log diff ke ClickHouse.
  • Validation 8 minggu sebelum cutover.

Cutover: traffic flip per-endpoint via gateway. Tidak ada user-facing downtime.

Hasil Q4: 1 modul (e-faktur) migrated. Latency e-faktur submit p95 turun dari 4.8 detik ke 720ms (Postgres index + connection pool tuning).

Year 2: domain core (12 bulan)

Setelah confidence dapat di Year 1, modul core mulai disentuh.

HR + payroll (4 bulan)

Modul HR + payroll punya regulatory complexity (BPJS, PPh21, PTKP). Pendekatan:

  1. Reverse engineer existing logic: 6 minggu kerja, baca 180k LOC PHP, dokumentasi business rule via decision tree.
  2. Write characterization tests: 1.200 test case yang capture current behavior legacy (termasuk bug yang sudah accepted sebagai feature).
  3. Implement di Spring Boot: 12 minggu kerja, tim 4 engineer.
  4. Shadow mode 6 minggu: kalkulasi payroll bulanan dilakukan paralel di legacy + modern, hasil di-compare. Discrepancy investigated.

Total discrepancy ditemukan saat shadow: 38 case. Semua resolved:

  • 12 bug legacy lama yang kami fix di modern (dokumentasi keputusan ke HR, restitusi ke karyawan affected).
  • 14 edge case yang modern initial implement salah (fix).
  • 8 dokumentasi business rule lama yang outdated (modern correct).
  • 4 selisih pembulatan (modern pakai banker’s rounding, legacy truncate — pilih banker’s setelah konsultasi finance team).

Cutover payroll month-end: April 2024. Smooth. Modul terbesar selama Year 2.

Inventory (4 bulan)

Inventory punya 380 tabel di legacy (jangan tanya kenapa — sejarah panjang). Schema modernization:

  • Konsolidasi ke 87 tabel (de-normalize beberapa lookup, normalize beberapa yang over-denormalized).
  • Implementation pakai jOOQ untuk type-safe SQL builder (lihat juga Spring Boot modular monolith).
  • Migrate data dengan custom ETL script + Postgres pg_dump | psql chain dengan transformation di tengah.

Production planning (4 bulan)

Modul paling tricky karena tightly coupled dengan inventory + HR (shift scheduling). Wait sampai keduanya migrate dulu. Saat HR + inventory done, production planning lebih straightforward (consumer dependencies, bukan owner).

Year 3: finishing + decommission (12 bulan)

Modul kecil sisanya (8 bulan)

Document management, audit log, supplier portal, customer portal, dashboard reporting. Tiap modul 3-6 minggu kerja.

Data archive + legacy decommission (3 bulan)

Setelah semua modul migrate, legacy masih running sebagai read-only untuk historical query.

  • Archive data legacy (2017-2023) ke Postgres archive instance.
  • Document data lineage di runbook.
  • Shutdown PHP + MySQL legacy.

Final audit + handover (1 bulan)

  • ISO 27001 re-audit: pass.
  • Documentation complete.
  • Training internal team final batch.

Strangler Fig: API Gateway dalam detail

OpenResty config (Lua):

local route_map = {
    {pattern = "^/hr/", upstream = "modern_app"},
    {pattern = "^/payroll/", upstream = "modern_app"},
    {pattern = "^/inventory/v2/", upstream = "modern_app"},
    {pattern = "^/inventory/", upstream = "legacy_php"},  -- old endpoint
    {pattern = "^/efaktur/", upstream = "modern_app"},
    {pattern = "^/production/", upstream = "legacy_php"},
    {pattern = "^/finance/", upstream = "legacy_php"},
    {pattern = "^/.*", upstream = "legacy_php"},  -- default catch-all
}

local function route_request()
    local uri = ngx.var.uri
    for _, route in ipairs(route_map) do
        if string.match(uri, route.pattern) then
            ngx.var.upstream = route.upstream
            return
        end
    end
end

route_request()

Plus shadow traffic untuk endpoint yang masih dalam validation:

local function shadow_mirror(uri, upstream_primary, upstream_shadow)
    -- Primary handle response
    -- Shadow get same request async, response diff logged
    ngx.timer.at(0, function()
        local httpc = require("resty.http").new()
        httpc:set_timeout(5000)
        local res, _ = httpc:request_uri(upstream_shadow, {
            method = ngx.var.request_method,
            body = ngx.req.get_body_data(),
            headers = ngx.req.get_headers()
        })
        -- Log to ClickHouse
        log_diff(uri, primary_response_hash, res and ngx.md5(res.body) or "error")
    end)
end

Decisions yang penting

1. Tidak rewrite database schema 1:1

Legacy schema penuh design quirks (column name di 47 tabel berbeda, naming PascalCase + snake_case mix, kolom is_deleted_flag_yn dengan value ‘Y’ atau ‘N’ bukan boolean).

Modernize schema saat migrate per-modul. Maps dari legacy → modern via ETL transformation, bukan blind 1:1.

Risk: schema modern tidak match legacy 1:1 = data sync bidirectional rumit. Mitigation: dual-write window dimana write goes through application service layer yang aware kedua schema.

2. Dual-write window per modul

Untuk setiap modul, ada window dimana write goes ke kedua DB. Periode: 2-6 bulan tergantung modul.

Trade-off: latency write naik (dua DB tulis), tapi enable easy rollback kalau bug ditemukan.

3. Tidak migrate semua data historis ke schema baru

Data sebelum 2018 (7 tahun) archive only, tidak di-migrate ke schema baru. Read access via dedicated archive read API.

Saving: 1.4 TB data tidak perlu di-transform. Trade-off: report cross-period (post-2018 + archive) butuh aplikasi-level join.

4. Keep PHP + MySQL knowledge in-house selama project

Engineer senior internal (yang mau pensiun) di-extend kontrak full 3 tahun untuk support legacy. Investment ~Rp 360 juta total — murah dibanding risk lose institutional knowledge mid-project.

Yang break (banyak, ini cuma top 5)

1. Character encoding hell

Legacy MySQL pakai latin1 + UTF-8 mixed (depending on table). Setelah migrate ke Postgres UTF-8, beberapa nama karyawan ditampilkan “M??lik” instead of “Malik”.

Fix: manual cleanup script per table, dengan visual review oleh HR team. 3 minggu kerja.

2. Timezone confusion

Legacy server timezone Asia/Jakarta, Postgres server UTC, application JVM TZ accidentally set di salah satu instance. Report shift produksi shift-3 (23:00-07:00) muncul split di 2 hari beda.

Fix: standardize semua component ke Asia/Jakarta system-wide, dokumentasi tegas, plus test case timezone-explicit.

3. Implicit timezone in MySQL TIMESTAMP

MySQL TIMESTAMP simpan internal sebagai UTC tapi convert ke session timezone saat SELECT. Postgres TIMESTAMP WITHOUT TIME ZONE literal value.

Migration: convert via ETL — CONVERT_TZ(timestamp_col, '+00:00', '+07:00') di source side sebelum dump.

4. PDF generation untuk faktur

Legacy pakai TCPDF dengan template specific yang punya quirky margin. Modern pakai Apache POI / iText. Output PDF beda secara visual (font, margin, header alignment).

Customer demand exact 1:1 (compliance gov: PDF template harus mirip yang sebelumnya). Akhirnya pakai library Java + iText dengan template tuning manual 3 minggu untuk match legacy exact.

5. Print receipt thermal printer integration

Pabrik pakai thermal printer Epson TM-T82 lewat parallel port (literally, parallel port di 2024). Legacy PHP echo raw ESC/POS command. Modernize butuh ulang protocol implementation di Java.

Fix: bridge service kecil (Node.js) untuk handle printer protocol, di-host di gateway level. Modern app HTTP POST ke bridge, bridge translate ke ESC/POS.

Hasil 3 tahun

  • 47 modul migrated: 100%
  • Big-bang downtime: 0
  • Maintenance window per modul: avg 4 jam (akhir pekan)
  • User satisfaction: dari NPS -12 ke +28
  • Response time p95 (typical operation): 4.2s → 380ms
  • Annual maintenance cost: turun ~32% (less ad-hoc patch, better tooling)
  • Audit ISO 27001: pass
  • Engineer team: dari 1 (legacy) ke 12 (modern + 1 legacy expert sampai pensiun)

Estimasi cost

  • Engineering: 12 FTE × 3 tahun ≈ Rp 14.4 miliar
  • Infrastructure: Rp 480 juta total (3 tahun, gradual upgrade)
  • License + tooling: Rp 240 juta total
  • Training + change management: Rp 180 juta
  • Total: Rp 15.3 miliar ($1 juta USD)

Bukan murah. Tapi alternatif (continue legacy, vendor lock-in baru, atau replace pakai SAP/Oracle) estimasi 2-4x lebih mahal.

Verdict

Modernize ERP legacy via Strangler Fig: feasible dalam 3 tahun untuk codebase 2M LOC + 47 modul + tim 10-15. Big-bang rewrite untuk skala ini hampir selalu gagal.

Investasi besar di upfront (tooling, observability, gateway) Year 1 adalah enabler. Tanpa itu, sisanya juga gagal.

Bukan magic. Banyak komite, banyak kompromi politik (mana modul migrate dulu = mana department dulu di-prioritize), banyak weekend kerja saat data migration. Tapi sustainable, vs death march big-bang yang banyak project legacy lain alami.

Lihat juga ADR architecture decision records untuk konteks bagaimana 200+ keputusan teknis di project ini didokumentasi.

Ditulis oleh Reza Pradipta