opn-ooo / go-boilerplate

6 stars 0 forks source link

Repository and Service pattern #1

Open sirn opened 3 years ago

sirn commented 3 years ago

During development of other projects, which were using the similar stack and infrastructure as Go-Boilerplate, we found several design issue with the way Repository is currently designed. Currently Repository implicitly passing gorm.DB and implements primitives such as Find, ByIDs, ByIsEnabled, Preload. This makes repository hard to extend, and unable to encapsulate a TRANSACTION without defining extra functions for that purpose (e.g., FindTx where it accepts transaction with first argument). For example:

handler.PostRepository.Get(
    repositories.ByID(id),
    repositories.Limit(limit),
)

In this example, gorm.DB is not explicitly passed to PostRepository and thus unable to utilises transaction at all. In the template, there is invalid usage of transaction, e.g. in Create:

https://github.com/opn-ooo/go-boilerplate/blob/b80c4106074ffe72c60cef87d0823f40a538a7d6/templates/database/repository.tmpl#L53-L62

At a glance, this code has two problems:

Additionally, in case of SQL standard-compliant SQL servers (anything but MySQL/MariaDB), TRANSACTION does more than grouping INSERT/UPDATE operations together for easier rollback, but also provides isolation for SELECT queries, ensuring data is not changing after BEGIN between the first and subsequent selects preventing subtle race condition issues (see Transaction Isolation).

Apart from that, we found several more design patterns that are hard to accomplish with the current repository design:

Suggestions

My suggestion for Repository refactor based on experience working with other projects codebase and team feedback, is that it should act more like a Service, in that they should be grouped based on business function. For example, in case of OrderRepository, with current structure we'd have:

In this case, since Repository is providing a primitives for querying data, business logic is instead implemented on the Handler. Some functions that may also resulted in being implemented in Handler instead, which may result duplicated code if such code were to be reused elsewhere (e.g., a Pay function).

If we were to refactor this code based on few rules:

This repository would be refactored into:

In this case, Handler would only handle processing request parameters (e.g., validating params, pagination) and creating a response struct (in case of API). In order to accommodate for migration to microservice in the future, we might also have few more rules:

In other words,

appdesign

I think by switching Repository to use Service pattern, we will be able to have much cleaner architecture for our services, as boundaries are clearly defined, and can migrate to service much easier later on. e.g., by simply wrapping a service with a Remote Facade.

takaaki-mizuno commented 3 years ago

Thanks for your feedback. I totally agree with your opinion about the transaction part. The current structure is not good for making transactions.

However, I still hesitate to remove the repository layer because the repository layer can conceal "where the data comes from ( database / API ...). Because the model itself is already a DTO and doesn't have to tie with the database tables.

Now I am thinking about how to support transactions in the right way on the repository layer.

boolafish commented 3 years ago

One issue though, is testing. Coupling data access layer and service layer you will need to test business logic and your SQL logic all together. Could be okay, but ideally they can be tested separately (and easier).

TIL about the Gorm transaction, such a non-user-friendly interface 😢

But not fully focusing on the service <> repo discussion, I do feel that the opn-generator might be a bit too opinionated so sometimes if the default generated code does not work out, engineer (aka, I) would write new functions, and it might be hard to clean up the unused codes 🤔