Saat aplikasi Go Fiber mulai dipakai lebih serius, pertanyaan yang sering muncul bukan lagi bagaimana membuat endpoint, tetapi kapan arsitektur yang sekarang mulai tidak cukup. Banyak tim terlalu cepat memecah aplikasi menjadi banyak service, padahal masalahnya bisa diselesaikan dengan modular monolith atau queue worker. Sebaliknya, ada juga tim yang menahan semua beban di satu proses sampai notifikasi, export, atau integrasi pihak ketiga mulai mengganggu request utama.

Untuk mayoritas tim kecil hingga menengah, urutannya biasanya sederhana: mulai dari modular monolith, tambahkan queue worker untuk proses async, lalu pisahkan menjadi service hanya jika ada alasan operasional yang jelas. Dengan pendekatan ini, Anda bisa menjaga kompleksitas tetap rendah tanpa mengorbankan skalabilitas saat aplikasi tumbuh.

Memahami tiga opsi arsitektur di Go Fiber

1. Modular monolith

Modular monolith adalah satu aplikasi deployable, satu codebase, biasanya satu proses utama API, tetapi dibagi menjadi modul yang jelas. Misalnya: user, order, notification, reporting. Setiap modul punya handler, service, repository, dan kontrak internal sendiri.

Di Go Fiber, ini sangat cocok karena Fiber hanya menjadi lapisan HTTP. Logika bisnis tetap ditempatkan di package internal yang terpisah, sehingga aplikasi tidak berubah menjadi kumpulan handler yang saling memanggil langsung.

2. Service terpisah

Service terpisah berarti sebagian domain atau kapabilitas dipindahkan ke aplikasi/proses deployable lain. Komunikasinya bisa lewat HTTP, gRPC, atau messaging. Contoh: layanan sinkronisasi pihak ketiga dipisah dari API utama karena kegagalan atau latensinya tidak boleh mengganggu CRUD utama.

Ini bukan keputusan yang salah, tetapi kompleksitasnya naik cukup tajam: deployment bertambah, observability perlu lintas proses, dan debugging tidak lagi cukup melihat satu log stream.

3. Queue worker

Queue worker adalah proses terpisah yang mengambil pekerjaan dari antrean untuk diproses secara asynchronous. Ini ideal untuk pekerjaan yang tidak perlu selesai di dalam siklus request-response, seperti kirim email, generate PDF, export report, atau sinkronisasi ke vendor eksternal.

Queue worker sering menjadi langkah paling masuk akal sebelum memecah service, karena Anda mendapatkan isolasi beban dan retry mechanism tanpa harus membelah domain aplikasi terlalu dini.

Kapan modular monolith masih menjadi pilihan terbaik

Untuk API CRUD biasa dengan beberapa integrasi pendukung, modular monolith biasanya masih yang paling efisien. Misalnya aplikasi internal dengan fitur autentikasi, pelanggan, invoice, dan dashboard admin. Semua fitur itu bisa hidup dalam satu aplikasi Fiber selama batas antarmodul dijaga dengan disiplin.

Tanda bahwa modular monolith masih cukup

  • Tim masih kecil, misalnya beberapa backend engineer yang mengerjakan codebase yang sama.
  • Mayoritas fitur berbagi database dan transaksi yang sama.
  • Deployment tunggal masih mudah dikelola.
  • Masalah utama belum pada isolasi failure, tetapi pada struktur kode yang mulai berantakan.
  • Kebutuhan skalabilitas masih lebih banyak pada throughput API, bukan pemisahan domain operasional.

Kelebihan modular monolith

  • Deployment sederhana: satu artefak, satu pipeline, satu konfigurasi utama.
  • Debugging lebih mudah: log, tracing, dan stack trace berada di satu proses.
  • Latensi internal rendah: pemanggilan antarmodul cukup function call, bukan network call.
  • Biaya operasional rendah: tidak perlu banyak service discovery, gateway, atau orchestration tambahan.
  • Cocok untuk tim kecil: lebih sedikit beban koordinasi antarrepo dan antarservice.

Kekurangan modular monolith

  • Bila tidak disiplin, modul saling bocor dan bergantung langsung ke detail internal modul lain.
  • Kegagalan satu proses bisa memengaruhi seluruh aplikasi.
  • Scaling dilakukan pada satu unit besar, walau bottleneck hanya di satu area.
  • Proses berat seperti export besar atau sinkronisasi lambat bisa merusak latency API bila tetap dijalankan sinkron.

Contoh struktur modular monolith di Go Fiber

internal/
  app/
    server.go
  customer/
    handler.go
    service.go
    repository.go
  report/
    handler.go
    service.go
    repository.go
  notification/
    service.go
  platform/
    db/
    logger/
    queue/
cmd/
  api/
    main.go

Poin pentingnya bukan nama folder, tetapi batas dependensi. Handler tidak boleh langsung mengakses database jika service dan repository sudah ditetapkan. Modul report sebaiknya tidak membaca tabel modul lain secara liar tanpa kontrak yang jelas.

Kapan perlu menambah queue worker

Jika API utama mulai menunggu pekerjaan yang sebenarnya tidak harus selesai saat itu juga, queue worker hampir selalu lebih tepat daripada langsung memecah menjadi service penuh.

Kasus yang cocok untuk queue worker

  • Notifikasi: email, WhatsApp, push notification setelah event tertentu.
  • Export report: generate CSV/PDF yang memakan waktu.
  • Sinkronisasi pihak ketiga: kirim data ke vendor yang lambat atau tidak stabil.
  • Thumbnail, kompresi, atau pemrosesan file.
  • Retry workflow untuk pekerjaan yang rawan gagal sementara.

Mengapa queue worker efektif

Dalam request HTTP, pengguna peduli pada respons cepat dan andal. Jika setelah membuat invoice Anda juga harus mengirim email, membuat PDF, dan sinkron ke sistem partner sebelum memberi respons 200, maka latency akan naik dan peluang timeout ikut membesar. Dengan queue, API cukup menyimpan state utama lalu mendorong job untuk diproses di belakang layar.

Trade-off queue worker

  • Kelebihan: latency API turun, retry lebih mudah, beban berat terisolasi, worker bisa di-scale terpisah.
  • Kekurangan: ada eventual consistency, butuh idempotensi, debugging alur bisnis menjadi dua tahap, dan perlu pemantauan antrean.

Contoh alur sederhana: API CRUD + notifikasi async

// handler membuat order dan enqueue job notifikasi.
func (h *OrderHandler) Create(c *fiber.Ctx) error {
    var req CreateOrderRequest
    if err := c.BodyParser(&req); err != nil {
        return fiber.NewError(fiber.StatusBadRequest, "payload tidak valid")
    }

    order, err := h.orderService.Create(c.Context(), req)
    if err != nil {
        return err
    }

    // Jangan gagal-kan request utama hanya karena enqueue notifikasi bermasalah
    // kecuali notifikasi memang bagian dari kebutuhan bisnis yang wajib sinkron.
    _ = h.jobPublisher.Publish(c.Context(), "order.created", map[string]any{
        "order_id": order.ID,
        "email":    order.CustomerEmail,
    })

    return c.Status(fiber.StatusCreated).JSON(order)
}

Yang penting di sini adalah keputusan bisnis: apakah notifikasi wajib sukses sebelum order dianggap berhasil? Pada banyak sistem, jawabannya tidak. Maka queue worker adalah pilihan yang natural.

Hal yang wajib diperhatikan saat memakai queue worker

  • Idempotensi: job bisa diproses dua kali. Pastikan efek samping tidak menggandakan email, invoice, atau sinkronisasi.
  • Retry policy: bedakan error sementara dan permanen.
  • Dead-letter handling: job yang gagal berulang perlu ditandai dan ditangani.
  • Status bisnis: untuk export report, simpan status seperti queued, processing, done, failed.
  • Correlation ID: bawa ID request atau ID job ke log agar tracing lebih mudah.

Kapan service terpisah benar-benar layak

Service terpisah layak dipilih saat ada kebutuhan operasional yang tidak bisa diselesaikan dengan modular monolith plus worker. Bukan karena ingin terlihat modern, tetapi karena memang ada batas domain, beban, atau failure mode yang berbeda.

Contoh kasus yang masuk akal

  • Sinkronisasi pihak ketiga punya dependensi, rate limit, credential, dan pola deployment berbeda dari API utama.
  • Mesin report membutuhkan resource CPU/memori yang jauh lebih tinggi dan release cycle berbeda.
  • Domain tertentu harus diisolasi karena kebutuhan keamanan atau ownership tim yang sudah jelas.
  • Skalabilitas berbeda jauh: misalnya public API trafiknya tinggi, tetapi back-office service rendah namun kompleks.

Kelebihan service terpisah

  • Failure isolation lebih baik: gangguan pada satu service tidak selalu menjatuhkan proses lain.
  • Scaling lebih presisi: hanya service yang berat yang ditambah replika.
  • Batas domain lebih tegas: lebih mudah menjaga ownership dan lifecycle berbeda.

Kekurangan service terpisah

  • Deployment lebih rumit: pipeline, environment variable, secret, health check, dan rollback bertambah.
  • Observability lebih mahal: perlu log aggregation, tracing lintas service, dan metrik yang konsisten.
  • Debugging lebih sulit: bug bisa muncul dari timeout, retry, atau kontrak antarservice.
  • Latensi bertambah: function call berubah menjadi network call.
  • Biaya operasional naik: infrastruktur, monitoring, dan beban koordinasi tim ikut naik.

Jika masalah utama Anda hanya “request terlalu lama karena kirim email dan export PDF”, memecah service penuh biasanya terlalu jauh. Tambahkan queue worker lebih dulu.

Matriks keputusan: modular monolith, service, atau queue worker?

KriteriaModular MonolithQueue WorkerService Terpisah
Kompleksitas deploymentRendahSedangTinggi
Latency request utamaBaik untuk proses sinkron ringanSangat baik untuk proses asyncTergantung komunikasi antarservice
Failure isolationRendahSedangTinggi
DebuggingPaling mudahLebih sulit karena asyncPaling kompleks karena distribusi
ObservabilitySederhanaButuh monitoring antrean dan jobButuh tracing dan metrik lintas service
Biaya operasionalRendahSedangTinggi
Cocok untuk tim kecilSangat cocokCocok jika kebutuhan async nyataHanya jika alasannya kuat
Use case utamaCRUD, dashboard, admin APINotifikasi, export, integrasi lambatDomain/operasi yang benar-benar perlu isolasi

Sinyal bahwa arsitektur saat ini mulai tidak cocok

Modular monolith mulai kewalahan jika:

  • Endpoint CRUD sederhana ikut lambat karena proses sampingan yang berat.
  • Deploy kecil sering berisiko mengganggu semua fitur.
  • Satu modul sering menyebabkan konsumsi CPU/memori yang memengaruhi modul lain.
  • Logika antarbagian terlalu saling terkait dan sulit diuji terpisah.

Queue worker mulai tidak cukup jika:

  • Worker memiliki lifecycle deployment, dependensi, atau ownership yang sangat berbeda.
  • Kontrak bisnisnya tumbuh menjadi domain tersendiri, bukan sekadar proses async.
  • Kapasitas atau SLA komponen itu perlu diatur independen dari aplikasi utama secara penuh.

Service terpisah justru menjadi beban jika:

  • Tim kecil harus mengelola terlalu banyak repo dan pipeline.
  • Banyak perubahan fitur membutuhkan koordinasi lintas service untuk hal yang sebenarnya sederhana.
  • Insiden makin sulit didiagnosis karena tracing belum matang.

Panduan praktis memilih untuk beberapa skenario nyata

1. API CRUD internal perusahaan

Pilih modular monolith. Fokus pada pemisahan modul, validasi, transaksi database, testing service layer, dan struktur package yang rapi. Jangan pecah service jika seluruh domain masih berbagi model data dan dikelola tim kecil yang sama.

2. Registrasi pengguna + kirim email/verifikasi

Gunakan modular monolith + queue worker. Endpoint registrasi sebaiknya menyimpan user lebih dulu, lalu enqueue email verifikasi. Ini menjaga respons tetap cepat dan menghindari kegagalan SMTP menjatuhkan request utama.

3. Export report penjualan bulanan

Gunakan queue worker. Endpoint cukup membuat record export dengan status queued. Worker memproses file di belakang layar, menyimpan hasilnya ke object storage atau filesystem, lalu status diubah menjadi done. Klien bisa polling status atau menerima notifikasi saat selesai.

4. Sinkronisasi data ke sistem pihak ketiga yang sering lambat

Mulai dari queue worker jika alurnya masih bagian dari domain utama. Pisahkan menjadi service terpisah bila integrasi itu sudah punya kebutuhan retry kompleks, credential rotation, rate limiting khusus, jadwal sinkronisasi sendiri, dan sering diubah tanpa kaitan langsung ke API utama.

Langkah migrasi bertahap tanpa overengineering

Tahap 1: rapikan modular monolith terlebih dahulu

  • Kelompokkan kode berdasarkan domain, bukan berdasarkan layer global yang terlalu umum.
  • Buat service layer yang jelas untuk aturan bisnis.
  • Batasi akses database melalui repository atau abstraction yang konsisten.
  • Tambahkan logging terstruktur dan context propagation.

Tahap 2: pindahkan proses lambat ke queue worker

  • Identifikasi endpoint yang sering lambat karena pekerjaan sampingan.
  • Definisikan payload job yang kecil dan jelas.
  • Simpan status proses bila hasilnya perlu dilacak pengguna.
  • Terapkan retry dan idempotensi sejak awal.

Tahap 3: ukur sebelum memisahkan service

  • Lihat metrik latency, error rate, throughput job, dan pola kegagalan integrasi.
  • Periksa apakah bottleneck benar-benar domain tertentu, bukan query database atau implementasi yang buruk.
  • Pastikan observability minimal sudah siap: log terstruktur, request ID, dashboard metrik, dan alert dasar.

Tahap 4: ekstrak service hanya pada bagian yang paling jelas batasnya

Jangan memulai dari modul yang paling sering dipakai bersama domain lain. Pilih area yang dependensinya paling mandiri. Misalnya modul sinkronisasi partner yang memang sudah asynchronous, punya tabel sendiri, dan jarang ikut transaksi langsung dari API CRUD.

Tips observability dan debugging yang sering terlupakan

  • Gunakan request ID/correlation ID di setiap request Fiber dan bawa nilainya ke job queue maupun panggilan service lain.
  • Log outcome bisnis, bukan hanya error teknis. Misalnya: report queued, report completed, partner sync failed.
  • Pisahkan metrik API dan worker. Jangan campur latency HTTP dengan durasi pemrosesan job.
  • Tentukan timeout dan retry secara eksplisit saat memanggil sistem eksternal.
  • Jangan sembunyikan kegagalan async. Jika job gagal, harus terlihat di dashboard, alert, atau status bisnis.

Kesalahan umum saat mengambil keputusan arsitektur

  • Terlalu cepat memecah service sebelum batas domain dan observability siap.
  • Menjalankan semua proses secara sinkron walau tidak dibutuhkan oleh pengguna akhir.
  • Menganggap queue menyelesaikan semua masalah padahal job yang tidak idempotent bisa menimbulkan bug baru.
  • Memecah berdasarkan tabel atau folder, bukan berdasarkan kebutuhan operasional dan batas domain.
  • Mengabaikan biaya tim kecil: semakin banyak proses deployable, semakin tinggi overhead koordinasi.

Rekomendasi praktis

Jika Anda membangun backend dengan Go Fiber dan aplikasi mulai tumbuh, keputusan yang paling aman biasanya:

  1. Mulai dengan modular monolith yang rapi.
  2. Tambahkan queue worker untuk notifikasi, export, dan integrasi lambat.
  3. Pisahkan menjadi service hanya bila ada kebutuhan isolasi operasional, scaling, atau ownership yang benar-benar jelas.

Dengan urutan ini, Anda mengurangi risiko overengineering sambil tetap membuka jalan untuk evolusi arsitektur. Tujuan utamanya bukan terlihat canggih, tetapi menjaga sistem tetap mudah diubah, mudah dioperasikan, dan cukup andal untuk kebutuhan nyata.