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.04k stars 464 forks source link

What to do in "cyclic import" situation? #30

Closed frederikhors closed 3 years ago

frederikhors commented 3 years ago

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:

pkg/reservation
  /app
    /query
      /get_reservation.go
    /command
      /create_reservation.go
    /app.go
  /domain
    /reservation
      /repository.go
      /reservation.go
  /adapters
    /postgres
      /repository.go
      /reservation.go
  /ports
    /http
      /reservation.go
      /service.go

pkg/game
  /app
    /query
      /get_game.go
    /command
      /create_game.go
    /app.go
  /domain
    /game
      /game.go
      /repository.go
  /adapters
    /postgres
      /game.go
      /repository.go
  /ports
    /http
      /game.go
      /service.go

pkg/player/... is the same

with /pkg/reservation/domain/reservation/reservation.go:

import (
  "pkg/game/domain/game"
  "pkg/game/domain/player"
)

type Reservation struct {
  id int

  start time.Time
  end time.Time

  game *game.Game
  player *player.Player
}

and /pkg/game/domain/game/game.go:

import (
  "pkg/reservation/domain/reservation"
  "pkg/game/domain/player"
)

type Game struct {
  id int

  finalScore string

  reservation *reservation.Reservation
  players []player.Player
}

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 imports pkg/game/domain/game and game.go imports pkg/reservation/domain/reservation.

The big doubt is how to fix this cyclic import.

As read here I can:

What do you recommend?

This is a very common situation I think.

m110 commented 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.

frederikhors commented 3 years ago

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?

frederikhors commented 3 years ago

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.

m110 commented 3 years ago

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.

frederikhors commented 3 years ago

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?

m110 commented 3 years ago

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?

frederikhors commented 3 years ago

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?

m110 commented 3 years ago

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.

frederikhors commented 3 years ago

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?

m110 commented 3 years ago

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.

frederikhors commented 3 years ago

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.

frederikhors commented 3 years ago

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!

m110 commented 3 years ago

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.

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.

frederikhors commented 3 years ago

Thank you very much!