Client / Company

Flip

Category Project

Backend

Industry

Modular Services

Project Time

2024 - 2025

Daftar Isi

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.

Biller services level 1
Biller services level 1

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.

Biller services level 2
Biller services level 2

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 : 

Biller services level 3 with Proxy Pattern
Biller services level 3 with Proxy Pattern

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 proxyWe need to maintain so many repository
We can set specific case to specific adapterIf every proxy need to add new module, we need to add to many repository
Ownership of AdapterWill 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
910└── internal
11    ├── config
12    ├── console
13    ├── infrastructure
14    └── modules
15        ├── inquiry
16        ├── transfer
17        ├── ping
18        └── status_check

Pada gambar dibawah, adalah flow dari main.go memanggil modules yang sudah di define di internal.

Biller Adapter Repository
Biller Adapter Repository

Jika PDAM Adapter membutuhkan adjustment, maka tinggal mengubah module/part yang dibutuhkan saja.

PDAM memerlukan custom terkait flow transfer
PDAM memerlukan custom terkait flow transfer

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.

Add new biller adapter
Add new biller adapter

Jika ternyata kita butuh adjustment di layer layer tertentu, misalnya mapping response code, mapping request, dan lain sebagainya. Kita bisa handle ini disisi repository.

Layering like Puzzle
Layering like Puzzle

Jika kita ingin custom salah satu layer, kita tinggal ganti pieces of puzzle.

Puzzle Playground
Puzzle Playground

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
30

Pada 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 adapterWe need to check if this flow is possible in current deployment
We can set specific case to specific adapterShould 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.

Biller Adapter Core with implementation
Biller Adapter Core with implementation

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.sum
***

Yah 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 ManagementMultiple Repo, with specific contextSingle repo, with multiple context adapter Multi Repo, with specific context.
MaintenanceHard to maintenanceIf need to add new module in each bank, we need to adjust all of repositoryEasy 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 ReusabilityWe need to do copy and paste for every moduleShared code, but we can custom if any specific use caseFor general code, it is shared using modules. 
ScalabilityEasy, because there is no dependency and every repository is agnosticEasy, because the module is plug and playAnd it still has its own binary, so we can scale it as we goEasy, because the module is plug and play, and has own repository
FlexibilityEasy, because its repo is agnostic. So it should be very flexibleEasy, 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
ComplexitySimple at the beginning, but slightly complex in the endComplex at beginning, and simple in the endComplex only on the core module. For bank adapter, its very simple
DeploymentSimple, but need to create pipeline for each repoHard (?) because we need to make sure this flow is supported or not in devopsSimple, because its align with current conditions