ThreeDotsLabs / wild-workouts-go-ddd-example

Go DDD example application. Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
https://threedots.tech
MIT License
5.24k stars 479 forks source link

Seperate Go structs for the repository pattern #37

Closed emilgelman closed 3 years ago

emilgelman commented 3 years ago

Hello,

Let me start by saying that I really enjoy reading your book, I find it extremely useful and very well written.

We are trying to follow the clean architecture principles in our team, and we have a question about the correct way to separate between models.

Assuming we have the following layers:

  1. API layer - using http
  2. Service layer - contains business logic
  3. Persistence layer - using the Repository pattern

We have some confusion around the best practices around which models to use where.

For example, what we currently do:

  1. Accept a request in HTTP, decode the request body into a DTO struct which is defined in the api package.
  2. Convert this struct into a "domain" struct (using mapstructure or a plain function), and pass it to the Service layer.
  3. In the service layer, we need to call some function from the Repository layer.

Here comes the problem: Should the repository layer accept and return a domain model, or a database model? If it accepts and returns a domain model, each implementation is responsible for decoding it to the whatever it wants, which feels more robust. However, it means that the repository layer is now aware of the domain layer, which doesn't feel right.

So basically my question is: which of the following options is considered more correct:

  1. Pass the repository a service model, which means the implementation will decide on how to use it and return it

    type Repository interface {
    Insert(model ServiceModel) (ServiceModel,error)
    }
  2. Add a DbModel struct to the database package, which means that the service model instantiates it and passes it to the repository

    type Repository interface {
    Insert(model DbModel) (DbModel,error)
    }

Appreciate your response!

m110 commented 3 years ago

Hey @emilgelman! I'm really happy you've found the book useful.

Should the repository layer accept and return a domain model, or a database model?

The domain model.

However, it means that the repository layer is now aware of the domain layer, which doesn't feel right.

Actually, that's how it should be. Remember that your repository is not a proxy for database table's CRUD methods. It's responsible for storing and retrieving domain objects, so it needs to have some idea of your domain.

Clean architecture assumes that implementation details can know about the inner layers but not the other way around. If you'd follow the second option, the database model would leak into the service layer, and you don't want that.

image

I'm a bit confused whether you use a separate "service model" and "domain model" or it's the same one?

Convert this struct into a "domain" struct (using mapstructure or a plain function), and pass it to the Service layer.

I'm concerned your domain objects might not be properly encapsulated if you use mapstructure to fill them. Maybe that's why using them in the storage layer doesn't seem right to you? Usually, you'd use only constructors from domain to create them, and their state should change only by the object's methods.

In my recent article, I mentioned a few tips on separating the structs, perhaps it will clear some things out. If not, let me know. 🙂

emilgelman commented 3 years ago

Hi Miłosz,

Thank you for the response. After reading the article, it all makes sense, thanks again!