Overview
Kita ingin membuat semacam Biller Services (bukan service yg sebenarnya). Secara usecase flow bisa dibilang semuanya sama. Namun, terkadang ada biller yang mempunyai usecase yang sedikit unik dan kita perlu membedakan usecase biller tersebut.
Misalnya, kita punya biller untuk pembayaran PDAM. Lalu kita ada biller untuk pembayaran PLN. Nah usecase flow dari 2 biller ini sama, namun yang membedakan hanyalah sumber datanya saja. Let's say, PDAM akan nge hit API https://api.pdam.com dan PLN akan nge hit API https://api.pln.com. Hal ini bisa di solved dengan melakukan pembeda pada adapternya saja
Karena 2 biller ini sama, maka kita bisa simpulkan ini menjadi 1 API, yaitu kita anggap sebagai https://api.company.com/v1/billers
Nah saat ada penambahan biller baru, misalnya Biller Internet & TV Kabel, kita tinggal onboard master datanya saja. Namun, gimana kalau misalnya biller tersebut membutuhkan usecase flow yang berbeda?
Problems
Bagaimana, jika ada biller yg high traffic? Tentu ini akan impact ke biller yang lain.
Nah jika misalnya ada salah satu biller yang lagi high traffic gimana? Tentu hal ini akan impact ke biller yang lain juga, dan bahkan service ini bisa down.
Lalu agar service ini ga down gmna? Kita bisa membuat sistem auto scaling.
Apakah masalah selesai? Setidaknya untuk High Availability sudah, namun sebenarnya dari sisi cost ini akan meningkat karena kita nge scale up 1 services. Hal ini nanti akan kita solved dengan Proxy Pattern.
Lalu saat ada penambahan biller baru, misalnya Biller Internet & TV Kabel, kita tinggal onboard master datanya saja. Namun, gimana kalau misalnya biller tersebut membutuhkan usecase flow yang berbeda? Biasanya kita akan membuat 2 pendekatan :
- Ubah usecase, dengan penambahan conditional
- Bikin endpoint baru untuk internet & tv kabel
Agar tidak ada adjustmen yang significant, kita akan memilih untuk membuat endpoint baru (services baru) /v1/billers/internet-tv. Namun ini akan membuat client dari biller services akan kebingungan, karena pada biller umum dan biller specific, kita membuat endpoint yang berbeda.
Hal ini yang menjadi concern research yang saya lakukan
Current Conditions
Pada kondisi saat ini, masalah 1 dan 2 sebenarnya sudah solved. Architecture yang digunakan saat ini adalah :
Disini biller services tetap melakukan flow common dengan nge translate request, lalu akan meneruskan ke masing masing proxy providers. Nah setiap proxy akan nge adjust flow nya sendiri. Sehingga kita ga butuh banyak endpoint yg dihit dari client. Cukup API /v1/billers dan client tidak perlu melakukan routing antar biller. Jika ada biller yang high traffic, tinggal scale proxy nya saja. Dan tidak akan impact ke biller yang lainnya.
Nah muncul masalahnya adalah, dari cara ngelakukan onboarding terhadap biller baru. Untuk saat ini, yang dilakukan adalah clone template proxy, lalu bikin proxy baru sesuai biller, dan deploy. Jadi masing masing proxy bakal punya repository baru (karena bakal banyak ngelakuin re-write dari template).
Problemnya jika kita disuruh untuk onboard 10 biller dalam 1 bulan. Tentu ini akan sangat menyulitkan dan jika kita nge maintain lebih dari 10 repository yg agnostic, jika ada penambahan endpoint dimasing masing proxy, maka akan paintfull banget untuk nge update semua repo proxy nya.
Solutions
Untuk solusi yang saya pikirkan, approach yg saya berikan adalah :
- Code Generator
- Plugin Based Adapter (Mono Repo)
- Plugin Based Adapter (Multi Repo)
Code Generator
Solusi ini bisa mempersingkat solusi yang sudah ada, yaitu template repository. Jadi saat ingin nge onboard biller baru, kita cukup install code-gen nya dan jalankan perintah :
1$ biller-generator init
2clone biller and setup basic modules
3
4$ biller-generator add <supported modules>
5install module <supported modules>Namun, kita perlu setup effort yang lumayan besar diawal, karena perlu bikin Template Repository yang bisa disupport oleh code gen nya. Jadi kita bikin 2 repo, yaitu Template Repo dan Code Generator Repo.
Berikut Benefit dan Tradeoff dari pendekatan ini :
| Benefit | Tradeoff |
|---|---|
| We can easily custom of each proxy | We need to maintain so many repository |
| We can set specific case to specific adapter | If every proxy need to add new module, we need to add to many repository |
| Ownership of Adapter | Will invest much time to create Code Generator, but after that we can use it seamless |
| Easier Deployment (because related to current deployment flow) | |
| Code style is likely same in default | |
| We dont need to create from scratch |
Plugin Based Repository
Pendekatan ini berbasis plug & play. Untuk mono repo dan multi repo, sebenarnya code nya sama, hanya implementasinya saja yang berbeda. Jadi disini saya hanya akan membahas yang Mono Repository saja.
Kita dapat mengimplementasikan sebuah mono repository untuk mengelola seluruh biller adapter / proxy secara terpusat. Mengingat bahwa sebagian besar adapter biller memiliki perilaku yang serupa, fungsionalitas umum dapat ditempatkan dalam modul default. Sementara itu, jika terdapat kebutuhan khusus pada alur tertentu untuk biller tertentu, kita dapat menempatkannya dalam modul kustom. Pendekatan ini menyerupai arsitektur Plugin-Based Adapter, yang memungkinkan fleksibilitas sekaligus efisiensi dalam pengelolaan dan pengembangan.
Berikut contoh struktur directory yang digunakan pada Plugin Based Adapter
1.
2├── cmd
3│ ├── pdam
4│ │ ├── Dockerfile
5│ │ └── main.go
6│ └── example
7│ └── main.go
8├── customs
9│
10└── internal
11 ├── config
12 ├── console
13 ├── infrastructure
14 └── modules
15 ├── inquiry
16 ├── transfer
17 ├── ping
18 └── status_checkPada gambar dibawah, adalah flow dari main.go memanggil modules yang sudah di define di internal.
Jika PDAM Adapter membutuhkan adjustment, maka tinggal mengubah module/part yang dibutuhkan saja.
Dan jika kita ingin nge onboard biller baru, misalnya electricty, kita tinggal membuat entry point baru saja dan register-in seluruh modul yang dibutuhkan. Kita cukup membuat file main.go dan Dockerfile nya saja, lalu voilaa.. Biller Adapter baru berhasil di onboard.
Jika ternyata kita butuh adjustment di layer layer tertentu, misalnya mapping response code, mapping request, dan lain sebagainya. Kita bisa handle ini disisi repository.
Jika kita ingin custom salah satu layer, kita tinggal ganti pieces of puzzle.
Berikut contoh code yang digunakan
1func main() {
2 config.InitConfig()
3 cfg := config.GetConfig()
4 router := muxtrace.NewRouter(muxtrace.WithServiceName(cfg.App.Name))
5
6 // setup default modules
7 var listModules = modules.RegisterDefaultModules(router)
8 var modules = infrastructure.NewModule(
9 listModules...,
10 )
11
12
13 // add modules to server
14 var server = console.NewServer(cfg.App.Name, cfg.App.Port, modules, router)
15
16
17 // start server
18 server.Start()
19}Disample ini, kita mendaftarkan 3 default modules
1func RegisterDefaultModules(router *muxtrace.Router)
2 (modules []infrastructure.ModuleInterface)
3{
4 modules = []infrastructure.ModuleInterface{
5 ping.NewModule(router),
6 transfer.NewModule(router, charge.NewUsecase()),
7 check.NewModule(router, check.NewUsecase(check.NewCheckRepository())),
8 }
9 return
10}Dan jika kita ingin custom module nya, kita cukup menambahkan file didalam folder customs/<biller_name>/modules
1.
2├── cmd
3│ ├── pdam
4│ │ ├── Dockerfile
5│ │ └── main.go
6│ ├── pln
7│ │ ├── Dockerfile
8│ │ └── main.go
9│ └── example
10│ └── main.go
11├── customs
12│ ├── pdam
13│ │ └── modules
14│ │ ├── check
15│ │ │ └── repository.go
16│ │ └── transfer
17│ │ ├── handler.go
18│ │ ├── init.go
19│ │ └── usecase.go
20└── internal
21 ├── config
22 ├── console
23 ├── infrastructure
24 └── modules
25 ├── inquiry
26 ├── check
27 ├── ping
28 └── transfer
29
30Pada struktur code diatas, kita membuat custom module pada status check, hanya di layer repository. Lalu kita nge adjust juga custom module untuk module transfer.
Setelah kita custom, kita cukup mendaftarkan custom module tersebut di main.go
1func main() {
2 config.InitConfig()
3 cfg := config.GetConfig()
4
5 router := muxtrace.NewRouter(muxtrace.WithServiceName(cfg.App.Name))
6
7 var listModules = modules.RegisterDefaultModules(router)
8 var modules = infrastructure.NewModule(
9 listModules...,
10 )
11
12 modules = modules.AddCustomModule(
13 // custom module check with custom repository
14 check.NewModule(router, check.NewUsecase(customCheck.NewCheckRepository())),
15
16 // add new module
17 transfer.NewModule(router, transfer.NewUsecase()),
18 )
19
20 var server = console.NewServer(cfg.App.Name, cfg.App.Port, modules, router)
21
22 server.Start()
23}Setelah itu, silahkan jalankan code nya dan hasilnya seperti berikut
1$ go run cmd/pdam/main.go
2
3{"time":"2025-01-14T11:51:38.443585+07:00","level":"INFO","msg":"starting server","service":"pdam adapter"}
4{"time":"2025-01-14T11:51:38.443817+07:00","level":"INFO","msg":"starting module","module":"ping"}
5{"time":"2025-01-14T11:51:38.443846+07:00","level":"INFO","msg":"starting module","module":"inquiry"}
6{"time":"2025-01-14T11:51:38.443855+07:00","level":"INFO","msg":"starting module","module":"check"}
7{"time":"2025-01-14T11:51:38.44387+07:00","level":"INFO","msg":"starting module","module":"transfer"}
8{"time":"2025-01-14T11:51:38.444035+07:00","level":"INFO","msg":"server starting on port 8080"}| Benefit | Tradeoff |
|---|---|
| We can easily custom of each adapter | We need to check if this flow is possible in current deployment |
| We can set specific case to specific adapter | Should be careful if want to create adjustment on core module |
| Ownership of Adapter (in level folder) | As the number of bank adapters increases, the repository will also grow larger. |
| Code style is likely same with default | |
| We don't need to create from scratch | |
| We can create new module (with default flow) in a minutes | |
| Single Repo, easily to maintain | |
| Shared default logic. We can avoid redundant code |
Nah kondisi deployment saat ini sebenarnya melakukan 1 apps untuk 1 repo. Karena disini kita approach nya menggunakan 1 repo multi binary / dockerfile, maka ini akan sulit dilakukan dan perlu adjustment disisi SRE (Software Reliability Engineer)
Oleh sebab itu, saya mengubah pendekatannya dari yang mono repo, menjadi multi repo dengan pendekatan yang mirip. Perbedaannya hanya di maintain repository nya saja.
Adapter yang lain hanya cukup connect ke core adapter dengan menggunakan go get dan implement main.go, Dockerfile, dan config.yml
1.
2├── cmd
3│ └── main.go
4├── config.yml
5├── customs
6│ └── modules
7│ └── transfer
8│ └── usecase.go
9├── go.mod
10└── go.sumYah itu saja riset yang saya lakukan selama beberapa hari, outputnya adalah beberapa pilihan dengan plus minus masing masing. Berikut adalah personal recomendation terkait riset yang saya lakukan
Personal Recomendation
Personal rekomendasi saya adalah, menggunakan Plugin Adapter Mono Repo atau Multi Repo untuk nge handle biller adapters. Karena disini contextnya biller tersebut mempunyai modules yang sama, flow yang sama dan usecase yang sama. Dan jika ada implementasi yang berbeda, cukup membuat custom file nya saja.
| Aspect | Current Condition | Plugin Adapter - Mono | Plugin Adapter - Multi |
|---|---|---|---|
| Code Management | Multiple Repo, with specific context | Single repo, with multiple context adapter | Multi Repo, with specific context. |
| Maintenance | Hard to maintenanceIf need to add new module in each bank, we need to adjust all of repository | Easy to maintenanceIf need to add new module, only register-it to each adapter (main.go) | Easy to maintenanceJust only maintain core adapter for default modules, and others adapter will implement its own unique cases |
| Code Reusability | We need to do copy and paste for every module | Shared code, but we can custom if any specific use case | For general code, it is shared using modules. |
| Scalability | Easy, because there is no dependency and every repository is agnostic | Easy, because the module is plug and playAnd it still has its own binary, so we can scale it as we go | Easy, because the module is plug and play, and has own repository |
| Flexibility | Easy, because its repo is agnostic. So it should be very flexible | Easy, because we designed it as a plugin. So if any custom, we can register that custom module. | Easy, because we designed it as a plugin. So if any custom, we can register that as custom modulesAnd every bank adapters can create its own modules |
| Complexity | Simple at the beginning, but slightly complex in the end | Complex at beginning, and simple in the end | Complex only on the core module. For bank adapter, its very simple |
| Deployment | Simple, but need to create pipeline for each repo | Hard (?) because we need to make sure this flow is supported or not in devops | Simple, because its align with current conditions |