Closed frederikhors closed 3 years ago
Hey @frederikhors!
First of all, I'm happy to hear you've found our work useful. :)
Here's what I see looking at your structure: You've split your application into three bounded contexts (game, reservation, and player), even though they seem to be part of a single domain.
Perhaps you've separated them like this after looking at wild-workouts. We plan to cover bounded contexts soon, but we didn't do it yet, and the current repository structure might not be optimal (as you might know, we wanted to show some anti-patterns along the way, and having incorrect boundaries is one of them). I'm sorry if you've got confused by this.
I've also responded on the Reddit post you've linked and I suggested separating the structs in case of separate bounded contexts, but I don't think this is the case for your project.
Of course, I don't know your domain, so I might be wrong about it. But I suspect your game, reservation, and player are all part of a single bounded context. In this case, I'd suggest merging them all together to have a structure like this:
pkg/<domain-name>
/app
/query
/get_game.go
/get_reservation.go
/command
/create_game.go
/create_reservation.go
/app.go
/domain
/game.go
/repository.go
/reservation.go
/adapters
/postgres
/game.go
/repository.go
/reservation.go
/ports
/http
/game.go
/reservation.go
/service.go
You won't have issues with referring to the models, and it should still be organized enough so you're not lost.
Of course, it's possible that each of these are separate, complex domain. But in such scenario, you should never import things out of another bounded context's domain (like pkg/game
importing pkg/reservations/domain
). You would treat them as totally separate services, just accidentally kept in a single repository. But again, I don't think it's the case for your project.
Let me know what you think about it.
I can't wait to study your chapter on bounded contexts! Please hurry up! :P
As for the answer I have to tell you that they are very complex scenarios: I have given you only a very simple basic example.
So I think your answer is enlightening when you say:
You would treat them as totally separate services, just accidentally kept in a single repository.
I think the only solution at this point is to repeat all the definitions for each pkg, treating them as completely separate black boxes, which do not know each other.
Among other things, I also need this for when I want to use a pkg in another future project: let's say I wanna use a reservation
system for another Golang project.
Am I wrong?
Look at this answer on SO: https://stackoverflow.com/questions/68098606/what-to-do-in-cyclic-import-situation-using-ddd-and-go?noredirect=1#comment120361532_68098606:
It's also worth considering that if Reservation and Game are separate aggregates, they should only refer to each other by ID (and thus would not have a cyclic import). Meanwhile, if one is just a part of the other, then, because all access should be through the containing aggregate, there's no need for the inner one to contain a reference to the containing aggregate (and thus there's no cyclic import; and it may make sense to put them into the same package). – Levi Ramsey.
As for the answer I have to tell you that they are very complex scenarios: I have given you only a very simple basic example.
Alright, that's why I didn't want to make too much assumptions. :) It's always about the domain and you know it best.
I think the only solution at this point is to repeat all the definitions for each pkg, treating them as completely separate black boxes, which do not know each other.
This seems right, but there's one key question: how do you plan to get the other entities?
Levi is correct, but the answer assumes you'd merge the bounded contexts into one, which you mentioned you don't want to do.
how do you plan to get the other entities?
What do you mean?
Are you referring to Player
for example? And maybe a future Team
entity?
This is my doubt, in this case I'll duplicate the code for repositories too in this package.
And all this seems redundant.
Maybe this confusion is because I still don't know DDD well.
I'm trying to follow this article principles too: https://threedots.tech/post/microservices-or-monolith-its-detail/: it's amazing!
And if I deploy my app in a microservices or monolithic way I need to address this doubt once for all.
Made myself clear?
Let's say you duplicate the Game
and Player
structs inside pkg/reservation
, so you no longer have the import cycle issue. I guess you get the Reservation
model from postgres in this context, right? But how would you fetch Game
and Player
?
It is curious that YOU are asking questions to me! 😄
I can ask you the same thing. 😄
Do you consider the article obsolete and no longer useful?
In that project, the concept of intraprocess
is used for the monolith.
If I had to use microservices instead I would go with grpc
or http
.
Do you have different ideas?
I wanted to hear your idea. 😁
The article is not obsolete, and you could use all these methods. The reason I'm rising this is it seems all the main models are coupled to models from another domains. It's not bad in itself, but you need to be aware that you will rely on synchronous calls. It won't have much impact with the monolith approach, but if you decide to split into services some day, a network outage or a service being down can kill your entire application.
The part that worries me especially is the cyclic dependency between game and reservations. For me that's a sign these two areas would be better kept together to reduce the number of outgoing calls.
The other way of doing this is using events and building local read models in each domain. But this is a whole new topic.
Again, I'm not sure how complex the domain is and what exactly is happening there, but it's just one more thing to consider.
I got it! Very clear!
Can you point me a way / book / epub / site to learn (very quickly) how to identify bounded contexts and domains?
Martin Fowler summarizes it well in https://martinfowler.com/bliki/BoundedContext.html
Bounded Contexts are usually a language boundary. Now that I think of it, perhaps in your example a "module" would be a better name for what you're currently doing?
An example of Bounded Contexts would be if you had a "users" context that handles login details and such, and then the "game" context that handles the game itself. In the game context, there's no users, but players. Both describe the same entity, but a Player is a projection of a User with only a subset of details (e.g., the game context doesn't bother about the user's password or email).
If you want a deep dive, Vaughn Vernon's Implementing Domain-Driven Design goes in depth into it.
Martin Fowler is as always precise, delicate and very useful. Thank you.
My ideas are becoming clearer and clearer.
Now the difficulty is figuring out when to use gameID int
(or maybe gameID *int
) references or if to use game *Game
in my structs.
Maybe we can add more complex examples in wild workout.
Something to better understand these mechanisms.
I can't wait to read your next article!
Now the difficulty is figuring out when to use
gameID int
(or maybegameID *int
) references or if to usegame *Game
in my structs.
Perhaps both, depending on the model? 🙂 You could keep a struct in the domain model, so your domain logic can make clear use of it. For storage, the ID is probably enough if you're going to fetch it from another source anyway.
Thank you very much!
I'm learning DDD and Golang thanks to your work! Thanks will never be enough!
I have a big doubt.
I'm building an app for tennis players and I have structured files like this:
with /pkg/reservation/domain/reservation/reservation.go:
and /pkg/game/domain/game/game.go:
At least for the moment this application is a simple CRUD and perhaps this structure is excessive. But soon the application will grow and I would like to be ready.
As you can see there is a cyclic import:
reservation.go
importspkg/game/domain/game
andgame.go
importspkg/reservation/domain/reservation
.The big doubt is how to fix this cyclic import.
As read here I can:
duplicate
Game
andReservation
structs: but I don't like this very much because they are perfectly the same, but maybe I'm wrong here;use everywhere IDs only: but this is not a fix, because sometimes I really need
*Reservation
in myGame
struct.What do you recommend?
This is a very common situation I think.