7
Lapisan Usecase, Model, dan Converter
Tiga use case di package usecase memegang aturan main aplikasi: UserUseCase, ContactUseCase, dan AddressUseCase. Ketiganya dirakit di satu tempat bersama DB, Log, dan Validate yang sama, lalu diserahkan ke controller HTTP dan middleware. internal/config/app.go:45-481 Halaman ini membahas pola yang dipakai ketiganya: satu transaksi per method, validasi request, pemanggilan repository, pengiriman event, lalu pemetaan lewat converter.
Satu transaksi per method
Setiap method publik membuka transaksi sendiri lewat c.DB.WithContext(ctx).Begin(), menunda tx.Rollback() dengan defer, dan menutupnya lewat tx.Commit(). Contoh paling ringkas ada di ContactUseCase.Create: transaksi dibuka, request divalidasi, entity disusun dari field request, lalu ContactRepository.Create dipanggil. internal/usecase/contact_usecase.go:37-632
sequenceDiagram
autonumber
participant UC as ContactUseCase.Create
participant DB as gorm.DB
participant V as validator.Validate
participant R as ContactRepository
participant CV as converter
participant P as ContactProducer
UC->>DB: Begin
UC->>V: Struct request
V-->>UC: error -> fiber.ErrBadRequest
UC->>R: Create tx dan contact
R-->>UC: entity.Contact terisi ID
UC->>DB: Commit
UC->>CV: ContactToEvent
UC->>P: Send event
UC->>CV: ContactToResponse
CV-->>UC: model.ContactResponse
Note over UC,P: producer nil melewati Send
%% fig: Siklus satu method use case
%% desc: Method membuka transaksi, memvalidasi request, memanggil repository, commit, lalu memetakan entity ke event dan response.Urutan sesudah Commit patut dicatat. ContactUseCase.Create baru mengirim event setelah transaksi selesai, dan hanya jika ContactProducer tidak nil. internal/usecase/contact_usecase.go:60-743 UserUseCase.Create memakai urutan yang sama: commit dulu, lalu converter.UserToEvent dan UserProducer.Send. internal/usecase/user_usecase.go:100-1164
⚠️ Event dikirim setelah
Commit. KalauSendgagal, method mengembalikanfiber.ErrInternalServerErrorpadahal baris database sudah tersimpan, sehingga client melihat kegagalan untuk pekerjaan yang sebenarnya berhasil. internal/usecase/contact_usecase.go:65-745
Ketergantungan yang dipegang use case
Ketiga struct use case memegang DB, Log, dan Validate, ditambah satu repository dan satu producer. AddressUseCase memegang dua repository karena setiap operasi address lebih dulu memastikan contact milik user tersebut ada. internal/usecase/address_usecase.go:49-536
| UseCase | Field repository | Field producer | Rujukan |
|---|---|---|---|
UserUseCase | UserRepository | UserProducer | internal/usecase/user_usecase.go:19-257 |
ContactUseCase | ContactRepository | ContactProducer | internal/usecase/contact_usecase.go:18-248 |
AddressUseCase | ContactRepository, AddressRepository | AddressProducer | internal/usecase/address_usecase.go:18-259 |
Constructor menerima producer sebagai pointer yang bisa kosong saat Kafka dimatikan. NewAddressUseCase menerima contactRepository lebih dulu, baru addressRepository, dan urutan itu harus diikuti pemanggilnya. internal/usecase/address_usecase.go:27-3810
Method publik tiap use case
| UseCase | Method | Request | Hasil |
|---|---|---|---|
UserUseCase | Verify | model.VerifyUserRequest | model.Auth |
UserUseCase | Create | model.RegisterUserRequest | model.UserResponse |
UserUseCase | Login | model.LoginUserRequest | model.UserResponse |
UserUseCase | Current | model.GetUserRequest | model.UserResponse |
UserUseCase | Logout | model.LogoutUserRequest | bool |
UserUseCase | Update | model.UpdateUserRequest | model.UserResponse |
ContactUseCase | Create, Update, Get | model.CreateContactRequest, model.UpdateContactRequest, model.GetContactRequest | model.ContactResponse |
ContactUseCase | Delete | model.DeleteContactRequest | error |
ContactUseCase | Search | model.SearchContactRequest | []model.ContactResponse, int64 |
AddressUseCase | Create, Update, Get | model.CreateAddressRequest, model.UpdateAddressRequest, model.GetAddressRequest | model.AddressResponse |
AddressUseCase | Delete | model.DeleteAddressRequest | error |
AddressUseCase | List | model.ListAddressRequest | []model.AddressResponse |
Verify adalah satu-satunya method yang mengembalikan model.Auth, yaitu struct berisi ID user hasil pencarian token. internal/model/auth.go:3-611 Pencarian itu memakai UserRepository.FindByToken di dalam transaksi yang sama. internal/usecase/user_usecase.go:38-5912
Login membandingkan password request dengan hash tersimpan lewat bcrypt.CompareHashAndPassword, mengisi user.Token dengan uuid.New().String(), menyimpannya lewat UserRepository.Update, commit, lalu mengembalikan converter.UserToTokenResponse yang hanya membawa token. internal/usecase/user_usecase.go:139-16113
Model: request, response, dan event
Package model memisahkan tiga bentuk data: request masuk, response keluar, dan event Kafka. Response dibungkus WebResponse atau PageResponse, dengan metadata paging di PageMetadata. internal/model/model.go:3-1914
UserResponse membawa ID, Name, Token, CreatedAt, dan UpdatedAt, semuanya bertanda omitempty sehingga field kosong hilang dari JSON. internal/model/user_model.go:3-915 ContactResponse dan AddressResponse sebaliknya tidak memakai omitempty pada field utama, dan ContactResponse bisa menyertakan Addresses. internal/model/contact_model.go:3-1216 internal/model/address_model.go:3-1217
Event dipisahkan dari response. UserEvent hanya membawa ID, Name, CreatedAt, dan UpdatedAt, tanpa token atau password. internal/model/user_event.go:3-1218 Ketiga event memenuhi interface model.Event yang hanya menuntut satu method, GetId(), dan method itulah yang dipakai producer sebagai key pesan. internal/model/event.go:3-519 internal/model/contact_event.go:3-1620
Producer generik memakai constraint itu: Send melakukan json.Marshal pada event, menyusun sarama.ProducerMessage dengan Key: sarama.StringEncoder(event.GetId()), lalu mengirimnya secara sinkron. internal/gateway/messaging/producer.go:21-4121 Topik ditentukan di constructor masing-masing producer; UserProducer mengunci topik users. internal/gateway/messaging/user_producer.go:10-2222
Converter memetakan entity ke dua arah keluar
Converter adalah fungsi bebas di package converter yang menerima pointer entity dan mengembalikan pointer model. Tidak ada state dan tidak ada akses database di dalamnya.
graph LR
E["entity.User"] --> UR["model.UserResponse"]
E --> UE["model.UserEvent"]
EC["entity.Contact"] --> CR["model.ContactResponse"]
EC --> CE["model.ContactEvent"]
EA["entity.Address"] --> AR["model.AddressResponse"]
EA --> AE["model.AddressEvent"]
style E fill:#336791,color:#fff
style EC fill:#336791,color:#fff
style EA fill:#336791,color:#fff
style UR fill:#2d7d46,color:#fff
style CR fill:#2d7d46,color:#fff
style AR fill:#2d7d46,color:#fff
style UE fill:#8b5cf6,color:#fff
style CE fill:#8b5cf6,color:#fff
style AE fill:#8b5cf6,color:#fff
%% fig: Pemetaan entity ke response dan event
%% desc: Setiap entity punya satu fungsi converter ke response HTTP dan satu ke event Kafka, dan keduanya membaca entity yang sama.UserToResponse menyalin ID, Name, CreatedAt, dan UpdatedAt; UserToTokenResponse hanya menyalin Token; UserToEvent menyalin empat field yang sama seperti response tanpa token. internal/model/converter/user_converter.go:8-3023 ContactToResponse dan ContactToEvent berbagi field yang sama, hanya ContactEvent menambahkan UserID. internal/model/converter/contact_converter.go:8-3124 AddressToEvent mempertahankan ContactId sedangkan AddressToResponse tidak membawanya. internal/model/converter/address_converter.go:8-3325
Untuk hasil berbentuk daftar, use case mengalokasikan slice response sepanjang hasil repository lalu mengisi tiap elemen dengan converter. ContactUseCase.Search melakukannya atas []entity.Contact. internal/usecase/contact_usecase.go:194-19926 AddressUseCase.List melakukannya atas []entity.Address. internal/usecase/address_usecase.go:214-21727
Validasi request
Validasi dijalankan dengan c.Validate.Struct(request) dan kegagalannya selalu dipetakan ke fiber.ErrBadRequest. Aturan ditegakkan lewat tag validate pada struct request, bukan di dalam method.
| Model | Field | Tag |
|---|---|---|
RegisterUserRequest | ID, Password, Name | required,max=100 |
UpdateUserRequest | Password, Name | max=100 |
SearchContactRequest | Page dan Size | min=1 dan min=1,max=100 |
CreateAddressRequest | PostalCode | max=10 |
Tag required pada UpdateUserRequest sengaja tidak ada supaya field kosong berarti jangan diubah. internal/model/user_model.go:15-2528 SearchContactRequest membatasi Size sampai 100. internal/model/contact_model.go:31-3829 Alamat membatasi PostalCode sepuluh karakter dan Country seratus karakter. internal/model/address_model.go:19-2730
Sebagian field request bertag json:"-". UserId pada request contact diisi oleh delivery layer dari sesi terautentikasi, bukan dari body. internal/model/contact_model.go:14-2031
Urutan validasi tidak seragam
ContactUseCase.Create memvalidasi request sebelum menyentuh database. internal/usecase/contact_usecase.go:41-4432 ContactUseCase.Update justru melakukannya setelah FindByIdAndUserId, sehingga contact yang tidak ada mengembalikan fiber.ErrNotFound lebih dulu daripada fiber.ErrBadRequest. internal/usecase/contact_usecase.go:83-9233
AddressUseCase.Create memvalidasi request sebelum menyentuh repository. internal/usecase/address_usecase.go:44-4734 AddressUseCase.Get sama sekali tidak memanggil validator: method itu langsung mencari contact dari request.ContactId dan request.UserId, lalu mencari address lewat AddressRepository.FindByIdAndContactId. internal/usecase/address_usecase.go:140-15435
List melakukan pencarian contact yang sama lalu memanggil AddressRepository.FindAllByContactId, juga tanpa Validate.Struct. internal/usecase/address_usecase.go:193-20136 Kedua method baca itu bergantung penuh pada filter kepemilikan data, bukan pada aturan tag validate.