Memilih struktur aplikasi di Go Fiber bukan sekadar soal selera folder. Keputusan antara modular monolith klasik dan vertical slice per domain akan memengaruhi kecepatan pengembangan, batas dependensi, cara tim melakukan testing, serta tingkat kesulitan refactor saat kode dan traffic bertambah.

Jawaban singkatnya: jika tim Anda masih kecil, domain bisnis belum terlalu kompleks, dan ingin alur yang familiar, modular monolith klasik biasanya lebih mudah dimulai. Jika fitur mulai banyak, ownership per domain makin penting, dan Anda ingin mengurangi coupling antar-layer global, vertical slice per domain biasanya lebih tahan terhadap pertumbuhan tim dan perubahan requirement. Keduanya tetap monolith; perbedaannya ada pada cara kode diorganisasi dan batas antar bagian dijaga.

Apa yang dibandingkan?

Dalam konteks artikel ini, keduanya sama-sama berjalan sebagai satu aplikasi Go Fiber, satu proses, dan umumnya satu database atau beberapa skema/database yang tetap diakses dari satu codebase. Fokusnya bukan microservices, melainkan cara menyusun kode agar perubahan fitur tidak cepat merusak area lain.

Modular monolith klasik

Pendekatan ini biasanya memisahkan aplikasi berdasarkan layer teknis global: handler, service, repository, model, middleware, dan seterusnya. Domain seperti user, order, atau product tersebar ke beberapa folder layer.

internal/
  handler/
    user_handler.go
    order_handler.go
  service/
    user_service.go
    order_service.go
  repository/
    user_repository.go
    order_repository.go
  model/
    user.go
    order.go
  middleware/
  routes/
cmd/
  api/
    main.go

Vertical slice per domain/fitur

Pendekatan ini mengelompokkan kode berdasarkan domain atau use case. Semua komponen yang diperlukan sebuah fitur diletakkan berdekatan: handler, service, repository, DTO, dan test berada di dalam slice yang sama.

internal/
  user/
    handler.go
    service.go
    repository.go
    entity.go
    dto.go
    routes.go
    handler_test.go
  order/
    handler.go
    service.go
    repository.go
    entity.go
    dto.go
    routes.go
  shared/
    database/
    auth/
    logger/
cmd/
  api/
    main.go

Perbedaan utamanya bukan pada pattern request-handler-service-repository, tetapi di mana pattern itu ditempatkan dan bagaimana dependensi antar bagian dibatasi.

Struktur alur request di Go Fiber

Baik modular monolith maupun vertical slice tetap bisa memakai alur yang sama:

  1. Route mendaftarkan endpoint Fiber.
  2. Handler membaca request, validasi input dasar, dan membentuk response HTTP.
  3. Service memuat aturan bisnis dan orkestrasi.
  4. Repository mengakses database atau dependency eksternal.

Contoh alur sederhana di Go Fiber:

func (h *UserHandler) Create(c *fiber.Ctx) error {
    var req CreateUserRequest
    if err := c.BodyParser(&req); err != nil {
        return fiber.NewError(fiber.StatusBadRequest, "invalid request body")
    }

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

    return c.Status(fiber.StatusCreated).JSON(user)
}
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*UserResponse, error) {
    if req.Email == "" {
        return nil, ErrInvalidEmail
    }

    exists, err := s.repo.EmailExists(ctx, req.Email)
    if err != nil {
        return nil, err
    }
    if exists {
        return nil, ErrEmailAlreadyUsed
    }

    user := User{Name: req.Name, Email: req.Email}
    if err := s.repo.Insert(ctx, &user); err != nil {
        return nil, err
    }

    return &UserResponse{ID: user.ID, Name: user.Name, Email: user.Email}, nil
}

Struktur ini valid untuk kedua pendekatan. Yang berubah adalah bagaimana file-file tersebut dibagi, diimpor, dan diuji.

Trade-off teknis: modular monolith klasik

Kelebihan

  • Mudah dipahami di awal. Banyak developer Go sudah familiar dengan pemisahan handler/service/repository secara global.
  • Onboarding cepat untuk aplikasi kecil. Folder lebih sedikit, konvensi mudah ditebak.
  • Komponen lintas domain tampak seragam. Misalnya semua repository ada di satu tempat, sehingga pola akses database mudah ditemukan.
  • Cocok untuk domain yang belum stabil. Saat bisnis masih eksploratif, struktur global sering terasa lebih cepat dibentuk.

Kekurangan

  • Coupling antar-domain mudah meningkat. Karena semua service atau repository berada di tempat global, developer cenderung saling memanggil langsung tanpa batas jelas.
  • Perubahan fitur menyebar ke banyak folder. Satu endpoint baru bisa menyentuh routes, handler, service, repository, model, validator, dan test di lokasi berbeda.
  • Sulit menjaga ownership tim. Ketika tim bertambah, siapa yang “memiliki” domain order atau billing jadi kabur karena semua berada pada layer bersama.
  • Refactor domain lebih mahal. Saat konsep bisnis berubah, kode terkait domain tersebar sehingga pencarian dependensi lebih sulit.

Risiko umum

Masalah paling sering pada modular monolith klasik adalah service layer global yang saling terhubung. Misalnya OrderService memanggil UserService, lalu UserService memanggil NotificationService, dan seterusnya. Secara fungsional mungkin bekerja, tetapi boundary bisnis menjadi kabur. Akibatnya:

  • unit test membutuhkan banyak mock,
  • perubahan kecil memicu efek samping luas,
  • circular dependency lebih mudah muncul pada level package.

Jika memilih modular monolith klasik, disiplin package boundary lebih penting daripada nama folder. Struktur yang sederhana tetap bisa sehat jika dependensi dijaga ketat.

Trade-off teknis: vertical slice per domain

Kelebihan

  • Boundary domain lebih jelas. Semua kode yang relevan untuk satu domain berada berdekatan.
  • Perubahan fitur lebih lokal. Menambah endpoint user umumnya cukup mengubah package user tanpa menyentuh banyak folder global.
  • Testing lebih fokus. Test per slice cenderung lebih mudah diorganisasi karena dependency terkait berada dalam konteks yang sama.
  • Lebih cocok untuk tim menengah. Ownership per domain atau per fitur lebih mudah diterapkan.
  • Mengurangi coupling horizontal. Developer lebih terdorong berinteraksi melalui interface atau use case yang eksplisit, bukan langsung memakai banyak komponen global.

Kekurangan

  • Ada potensi duplikasi. DTO, validator, atau helper kecil bisa muncul mirip di beberapa domain.
  • Perlu disiplin lebih tinggi pada shared code. Jika semua utilitas dipindah ke shared, vertical slice bisa kembali menjadi modular monolith terselubung.
  • Onboarding awal bisa lebih membingungkan bagi developer yang terbiasa berpikir per-layer teknis.
  • Abstraksi bisa berlebihan jika domain masih sangat kecil. Hasilnya justru folder banyak tetapi manfaatnya belum terasa.

Risiko umum

Masalah utama vertical slice biasanya bukan kekurangan struktur, melainkan terlalu cepat mengekstrak shared package. Misalnya karena ada dua fungsi validasi mirip, tim membuat package global generik yang dipakai semua domain. Lambat laun, package shared menjadi tempat campuran aturan bisnis. Ini menghapus manfaat boundary per domain.

Duplikasi kecil sering kali lebih murah daripada shared abstraction yang salah. Jika dua domain tampak mirip tetapi kemungkinan berubah dengan alasan bisnis berbeda, simpan terpisah dulu.

Dampak saat traffic dan jumlah developer bertambah

Saat traffic meningkat

Dari sisi performa runtime, pilihan modular monolith vs vertical slice tidak otomatis membuat aplikasi lebih cepat atau lebih lambat. Latency lebih dipengaruhi oleh query database, caching, concurrency, connection pool, dan efisiensi I/O.

Namun struktur kode tetap berpengaruh secara tidak langsung:

  • Debugging bottleneck lebih mudah jika boundary domain jelas.
  • Optimasi per fitur lebih aman saat dependency antar-domain tidak kusut.
  • Perubahan performa lebih terlokalisasi, misalnya menambah cache di domain order tanpa menyentuh user.

Dengan kata lain, vertical slice biasanya memberi keuntungan operasional pada fase optimasi, bukan karena framework lebih cepat, tetapi karena biaya memahami dampak perubahan lebih rendah.

Saat jumlah developer bertambah

Di sinilah perbedaannya lebih terasa:

  • Modular monolith klasik mulai berat ketika banyak developer mengedit folder yang sama, misalnya service atau repository global. Merge conflict dan perubahan lintas-area lebih sering terjadi.
  • Vertical slice lebih cocok untuk pembagian ownership. Satu developer atau squad bisa fokus pada domain tertentu tanpa terlalu sering masuk ke slice lain.

Untuk tim kecil, friction ini belum terlihat. Untuk tim menengah, struktur yang tidak jelas sering menjadi sumber perlambatan lebih besar daripada persoalan kode itu sendiri.

Maintainability, testing, dan dependency boundary

Maintainability

Modular monolith klasik lebih maintainable jika aplikasi masih sederhana dan tim konsisten menjaga aturan lintas-layer. Tetapi ketika domain berkembang, maintainability turun jika satu use case tersebar ke terlalu banyak tempat.

Vertical slice cenderung lebih maintainable untuk aplikasi dengan banyak fitur yang berubah cepat, karena konteks perubahan lebih terpusat.

Testing

Pada modular monolith klasik, test sering mengikuti layer teknis: test service di satu tempat, test repository di tempat lain. Ini baik untuk unit test murni, tetapi bisa membuat test skenario bisnis menyebar dan sulit dibaca dari perspektif fitur.

Pada vertical slice, Anda bisa menaruh test dekat dengan fitur:

  • test handler untuk kontrak HTTP,
  • test service untuk aturan bisnis,
  • test repository untuk integrasi database.

Hasilnya, saat domain order bermasalah, developer cukup membuka satu package untuk melihat implementasi dan test terkait.

Dependency boundary

Prinsip yang sebaiknya dijaga pada dua pendekatan:

  • handler tidak boleh mengetahui detail query database,
  • repository tidak boleh memuat aturan bisnis HTTP,
  • service sebaiknya bergantung pada interface yang relevan, bukan implementasi yang terlalu luas,
  • antar-domain berinteraksi lewat kontrak yang sempit dan jelas.

Jika package user membutuhkan data dari order, jangan langsung mengimpor separuh package order hanya untuk satu fungsi. Buat interface atau facade kecil yang menjelaskan kebutuhan sebenarnya.

Contoh struktur folder yang praktis di Go Fiber

Opsi 1: modular monolith klasik yang masih sehat

cmd/
  api/
    main.go
internal/
  routes/
    routes.go
  handler/
    user_handler.go
    order_handler.go
  service/
    user_service.go
    order_service.go
  repository/
    user_repository.go
    order_repository.go
  model/
    user.go
    order.go
  platform/
    database/
    logger/
    config/
  middleware/
  errs/
pkg/

Agar struktur ini tidak cepat kusut:

  • hindari service saling memanggil tanpa alasan kuat,
  • pisahkan DTO request/response dari entity database jika kebutuhan API sering berubah,
  • buat aturan package import yang jelas.

Opsi 2: vertical slice per domain/fitur

cmd/
  api/
    main.go
internal/
  user/
    handler.go
    service.go
    repository.go
    entity.go
    dto.go
    routes.go
    errors.go
    service_test.go
  order/
    handler.go
    service.go
    repository.go
    entity.go
    dto.go
    routes.go
  shared/
    database/
    auth/
    logger/
    config/
  middleware/
pkg/

Struktur ini cocok jika Anda ingin setiap domain lebih otonom. shared sebaiknya hanya berisi komponen teknis umum seperti koneksi database, logger, konfigurasi, atau helper yang benar-benar lintas-domain dan stabil.

Kapan modular monolith klasik lebih cocok?

  • Tim 1-3 developer.
  • Aplikasi masih tahap awal atau MVP yang berkembang cepat.
  • Domain bisnis belum banyak dan boundary belum jelas.
  • Tim lebih nyaman dengan struktur per-layer dan ingin onboarding secepat mungkin.
  • Kebutuhan testing dan ownership domain belum kompleks.

Pendekatan ini tetap masuk akal selama Anda sadar bahwa kesederhanaannya bisa menjadi utang desain jika domain bertambah tetapi struktur tidak ikut diperbaiki.

Kapan vertical slice per domain lebih cocok?

  • Fitur sudah banyak dan mulai saling bertabrakan.
  • Tim 4-10 developer atau lebih, dengan pembagian ownership area.
  • Perubahan requirement sering terjadi per domain, bukan merata di seluruh sistem.
  • Anda ingin test, code review, dan debugging lebih dekat ke konteks fitur.
  • Coupling antar-service global sudah mulai menyulitkan refactor.

Jika gejala berikut muncul, vertical slice biasanya layak dipertimbangkan:

  • satu tiket fitur mengubah banyak folder layer global,
  • merge conflict sering terjadi di folder service atau handler umum,
  • developer baru sulit menelusuri alur fitur end-to-end,
  • test bisnis tersebar dan tidak jelas pemiliknya.

Strategi migrasi bertahap tanpa rewrite besar

Anda tidak perlu memindahkan seluruh aplikasi sekaligus. Migrasi yang aman biasanya bersifat incremental.

1. Mulai dari fitur baru

Biarkan kode lama tetap di struktur lama. Untuk fitur baru, buat slice baru per domain. Ini cara termurah untuk menguji apakah pola baru cocok bagi tim.

2. Pilih satu domain yang paling sering berubah

Misalnya domain order sering memicu banyak refactor. Pindahkan domain ini dulu ke struktur vertical slice, sementara domain lain tetap modular klasik.

3. Buat boundary melalui interface

Sebelum memindahkan file, kecilkan dependensi. Contoh: daripada OrderService bergantung langsung pada package user yang besar, definisikan interface sempit seperti:

type UserLookup interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

Dengan interface kecil, pemindahan package menjadi lebih mudah karena contract sudah jelas.

4. Pindahkan test bersamaan dengan kode

Jangan memigrasikan implementasi tanpa memindahkan atau menulis ulang test. Test adalah jaring pengaman utama saat boundary berubah.

5. Hindari membuat package shared terlalu cepat

Jika ada kode yang tampak mirip di dua domain, tahan dulu. Pindahkan ke shared hanya jika pola benar-benar stabil dan alasan perubahannya sama.

6. Ubah wiring di composition root

Pusat inisialisasi dependency, route, database, dan middleware sebaiknya tetap terkonsentrasi di satu tempat, misalnya cmd/api/main.go atau package bootstrap. Ini memudahkan coexistence antara struktur lama dan baru.

func registerRoutes(app *fiber.App, db *sql.DB) {
    userRepo := user.NewRepository(db)
    userSvc := user.NewService(userRepo)
    userHandler := user.NewHandler(userSvc)
    user.RegisterRoutes(app, userHandler)

    orderRepo := order.NewRepository(db)
    orderSvc := order.NewService(orderRepo)
    orderHandler := order.NewHandler(orderSvc)
    order.RegisterRoutes(app, orderHandler)
}

Dengan composition root yang jelas, domain lama dan domain baru bisa hidup berdampingan selama masa transisi.

Kesalahan umum yang perlu dihindari

  • Menyamakan banyak folder dengan arsitektur baik. Struktur rapi tidak otomatis berarti dependency sehat.
  • Menaruh semua hal ke package shared. Ini mengembalikan coupling global.
  • Membuat abstraction terlalu dini. Interface atau helper generik yang salah justru memperlambat refactor.
  • Repository terlalu gemuk. Repository sebaiknya fokus pada akses data, bukan orkestrasi bisnis lintas-domain.
  • Handler memuat logika bisnis. Ini membuat test dan reuse aturan bisnis lebih sulit.
  • Refactor besar sekaligus. Risiko regresi tinggi dan sulit direview.

Tips debugging dan review arsitektur

  • Lihat satu perubahan fitur: berapa banyak file dan folder yang harus disentuh?
  • Periksa import antar-package: apakah domain saling bergantung terlalu dalam?
  • Cek test yang gagal saat satu domain berubah: apakah area dampaknya masih masuk akal?
  • Amati merge conflict: jika sering terjadi di layer global, itu tanda coupling organisasi kode mulai bermasalah.
  • Review package shared setiap beberapa sprint: apakah isinya benar-benar komponen umum atau aturan bisnis tersembunyi?

Checklist keputusan untuk tim kecil dan menengah

Checklist tim kecil

  • Apakah domain bisnis masih sederhana?
  • Apakah 1 fitur baru masih mudah ditelusuri meski tersebar ke beberapa layer?
  • Apakah tim lebih butuh kecepatan awal daripada boundary domain yang ketat?
  • Apakah service global belum saling mengunci terlalu banyak?

Jika mayoritas jawabannya ya, modular monolith klasik masih masuk akal.

Checklist tim menengah

  • Apakah ownership per domain mulai penting?
  • Apakah merge conflict sering terjadi di folder global?
  • Apakah satu perubahan bisnis sering berdampak ke banyak service lain?
  • Apakah onboarding developer baru lambat karena alur fitur tersebar?
  • Apakah test lebih mudah dipahami jika dikelompokkan per fitur?

Jika mayoritas jawabannya ya, vertical slice per domain biasanya memberi hasil lebih baik.

Kesimpulan

Pada Go Fiber, pilihan antara modular monolith vs vertical slice per domain bukan soal mana yang paling modern, tetapi mana yang paling sesuai dengan ukuran tim, kompleksitas domain, dan arah pertumbuhan aplikasi.

Modular monolith klasik cocok untuk memulai dengan cepat, terutama pada tim kecil dan domain yang belum kompleks. Vertical slice per domain lebih unggul ketika fitur bertambah, boundary bisnis perlu dijaga, dan biaya koordinasi antar-developer mulai terasa.

Jika ragu, jangan langsung rewrite. Mulailah dari satu domain yang paling sering berubah, jaga composition root tetap sederhana, dan gunakan test sebagai pengaman. Arsitektur yang baik bukan yang paling banyak pola, tetapi yang membuat perubahan fitur tetap aman, terukur, dan mudah dipahami beberapa bulan ke depan.