Struktur Project Golang Gin: Monolit yang Siap Jadi Microservice

Pendahuluan

Salah satu tantangan terbesar saat memulai project backend dengan Golang dan Gin adalah menentukan struktur folder yang tepat sejak awal. Banyak tim memilih untuk memulai dengan arsitektur monolit karena lebih sederhana untuk dikembangkan dan di-deploy, namun mereka juga ingin memastikan project tersebut microserviceable — artinya bisa dipecah menjadi layanan-layanan terpisah di kemudian hari tanpa perlu menulis ulang seluruh kode dari nol.

Artikel ini membahas pendekatan struktur project Gin yang mengadopsi prinsip modular monolith, sehingga di awal project berjalan sebagai satu aplikasi (monolit), tetapi setiap modul di dalamnya sudah didesain dengan batasan yang jelas (bounded context) sehingga mudah diekstrak menjadi microservice mandiri.

Prinsip Dasar

  1. Domain-Driven Design (DDD) ringan — setiap fitur bisnis dikelompokkan sebagai modul/domain, bukan berdasarkan tipe file (controller, model, dll di satu folder besar).
  2. Loose coupling antar modul — modul saling berkomunikasi lewat interface atau event, bukan memanggil struct/fungsi internal modul lain secara langsung.
  3. Shared kernel minimal — hanya hal-hal yang benar-benar umum (util, config, middleware) yang boleh dipakai bersama.
  4. Database per modul (logical separation) — meskipun pakai satu database di awal, skema/tabel dipisah per modul agar mudah dipisah ke database sendiri nanti.
  5. Dependency Injection — memudahkan mengganti implementasi (misalnya dari pemanggilan fungsi langsung menjadi HTTP/gRPC call) saat modul diekstrak jadi service terpisah.

Struktur Folder

project-root/
├── cmd/
│   ├── api/
│   │   └── main.go              # entry point monolit (menjalankan semua modul)
│   └── worker/
│       └── main.go              # entry point worker/consumer (opsional)
│
├── internal/
│   ├── modules/
│   │   ├── user/
│   │   │   ├── delivery/
│   │   │   │   └── http/
│   │   │   │       ├── handler.go
│   │   │   │       └── routes.go
│   │   │   ├── usecase/
│   │   │   │   └── user_usecase.go
│   │   │   ├── repository/
│   │   │   │   ├── user_repository.go
│   │   │   │   └── user_repository_impl.go
│   │   │   ├── entity/
│   │   │   │   └── user.go
│   │   │   ├── dto/
│   │   │   │   └── user_dto.go
│   │   │   └── module.go        # wiring dependency modul user
│   │   │
│   │   ├── product/
│   │   │   ├── delivery/http/
│   │   │   ├── usecase/
│   │   │   ├── repository/
│   │   │   ├── entity/
│   │   │   ├── dto/
│   │   │   └── module.go
│   │   │
│   │   └── order/
│   │       ├── delivery/http/
│   │       ├── usecase/
│   │       ├── repository/
│   │       ├── entity/
│   │       ├── dto/
│   │       └── module.go
│   │
│   ├── shared/
│   │   ├── middleware/
│   │   │   ├── auth.go
│   │   │   ├── logger.go
│   │   │   └── recovery.go
│   │   ├── response/
│   │   │   └── response.go      # format response API standar
│   │   ├── errors/
│   │   │   └── errors.go
│   │   ├── event/
│   │   │   └── event_bus.go     # komunikasi antar modul secara async
│   │   └── validator/
│   │       └── validator.go
│   │
│   └── config/
│       ├── config.go
│       └── database.go
│
├── pkg/
│   ├── logger/
│   ├── jwt/
│   └── httpclient/              # dipakai saat modul jadi service terpisah (HTTP client antar service)
│
├── migrations/
│   ├── user/
│   ├── product/
│   └── order/
│
├── docs/
│   └── swagger/
│
├── deployments/
│   ├── docker/
│   │   ├── Dockerfile.monolith
│   │   └── Dockerfile.user-service   # siap dipakai saat modul diekstrak
│   └── k8s/
│
├── .env.example
├── go.mod
├── go.sum
└── README.md

Penjelasan Tiap Bagian

cmd/

Berisi entry point aplikasi. Untuk mode monolit, cmd/api/main.go akan meng-inject dan menjalankan semua modul dalam satu proses Gin. Saat sebuah modul diekstrak jadi microservice, cukup buat entry point baru (misalnya cmd/user-service/main.go) yang hanya menjalankan modul user saja.

internal/modules/

Ini adalah inti dari pendekatan modular monolith. Setiap modul (user, product, order, dst) berdiri sendiri dengan layer:

  • delivery — handler HTTP (atau gRPC nanti) dan route.
  • usecase — business logic murni, tidak tahu-menahu soal HTTP atau database.
  • repository — interface + implementasi akses data.
  • entity — model domain.
  • dto — struct request/response.
  • module.go — tempat wiring dependency (constructor injection), berfungsi sebagai “pintu masuk” modul yang nantinya jadi titik ekstraksi ke service terpisah.

internal/shared/

Kode yang benar-benar lintas modul: middleware, format response, error handling standar, dan event bus sederhana (in-memory pub/sub) untuk komunikasi antar modul secara asynchronous. Event bus ini penting karena saat modul dipecah jadi microservice, event bus in-memory bisa diganti dengan message broker (RabbitMQ/Kafka) tanpa mengubah banyak logic di usecase.

Komunikasi Antar Modul

Aturan pentingnya: modul A tidak boleh mengimpor package internal modul B secara langsung (misalnya product memanggil struct repository milik order). Sebagai gantinya:

  • Panggil lewat interface yang didefinisikan di modul pemanggil, dan modul lain hanya menyediakan implementasi.
  • Atau lewat event bus untuk komunikasi async (misalnya order.created didengar oleh modul product untuk mengurangi stok).

Pola ini membuat modul mudah “dicabut” jadi service sendiri: interface tinggal diganti implementasinya dengan HTTP/gRPC client, dan event bus in-memory tinggal diganti message broker.

Database

Di tahap monolit, semua modul boleh memakai satu instance database, tapi:

  • Tabel dikelompokkan per modul (bisa pakai schema/prefix terpisah).
  • Migration dipisah per modul (migrations/usermigrations/product, dst).
  • Repository setiap modul hanya boleh query ke tabel miliknya sendiri, tidak melakukan JOIN lintas modul.

Aturan ini yang paling sering dilanggar tapi paling krusial — begitu ada JOIN lintas tabel modul berbeda, proses ekstraksi ke microservice jadi jauh lebih sulit.

deployments/

Menyiapkan Dockerfile terpisah sejak awal: satu untuk menjalankan seluruh monolit (Dockerfile.monolith), dan template untuk masing-masing service (Dockerfile.user-service, dst) yang baru benar-benar dipakai saat modul tersebut resmi dipisah.

Alur Migrasi dari Monolit ke Microservice

  1. Pilih modul dengan beban trafik tertinggi atau yang paling independen (biasanya user/auth atau notification).
  2. Buat entry point baru di cmd/ khusus modul tersebut.
  3. Ganti pemanggilan interface langsung dari modul lain menjadi HTTP/gRPC client (memanfaatkan pkg/httpclient).
  4. Pisahkan migration dan (jika perlu) database modul tersebut ke instance sendiri.
  5. Deploy modul sebagai service terpisah menggunakan Dockerfile yang sudah disiapkan.
  6. Modul yang masih di monolit tetap berjalan seperti biasa tanpa perubahan besar.

Contoh Potongan module.go

go

package user

import (
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

type Module struct {
    Handler *http.UserHandler
}

func NewModule(db *gorm.DB, router *gin.RouterGroup) *Module {
    repo := repository.NewUserRepository(db)
    uc := usecase.NewUserUsecase(repo)
    handler := http.NewUserHandler(uc)

    handler.RegisterRoutes(router)

    return &Module{Handler: handler}
}

Di cmd/api/main.go, setiap modul cukup di-mount seperti ini:

go

func main() {
    db := config.InitDB()
    router := gin.Default()
    v1 := router.Group("/api/v1")

    user.NewModule(db, v1.Group("/users"))
    product.NewModule(db, v1.Group("/products"))
    order.NewModule(db, v1.Group("/orders"))

    router.Run(":8080")
}

Saat user diekstrak jadi service sendiri, cukup buat cmd/user-service/main.go yang hanya memanggil user.NewModule(...) dan menjalankannya di port sendiri — tanpa menyentuh kode modul user itu sendiri.

Penutup

Struktur modular monolith seperti ini memberikan keseimbangan antara kecepatan development di tahap awal (satu repo, satu deployment, satu database) dengan fleksibilitas jangka panjang untuk scale-out per modul. Kuncinya ada pada disiplin menjaga batasan antar modul sejak awal — begitu batasan itu longgar (misalnya JOIN lintas modul atau import langsung antar package modul), proses migrasi ke microservice akan jauh lebih menyakitkan di kemudian hari.

News