pulling an owl on a globe?

One day I was sitting in a bar with an old friend with whom I had previously worked at a pose-pose-before last job. He is just one of those doubters, an ardent supporter of his current language. I want to say that he does really cool things, writes flawless code, he has a lot to learn. But his attitude to Go is not too positive. As he said: “Go is *****code (bad code)”. And as one of the arguments, he cited how, in his opinion, the error handling is implemented crookedly in Go. In some ways, he is right – in my current, not the largest Go project, the “if err != nil” construction occurs 1132 times.

This friend of mine, I’m not afraid of this word, is an adherent of DDD (domain driven design). Everything that does not belong to DDD is, in his opinion, an anti-pattern, hell and chaos. When I told him that I had a fairly successful experience in DDD design in Go projects, he rolled his eyes. Yes, I replied, with a certain series of caveats and compromises, it works, and not bad.

Hello, my name is Tolya and I am a leading developer of a payment service in Constant. My development experience is 15 years, I developed in PHP, in pure C, sometimes in C ++, and for the last 2 years I have been developing in Go.
It seems to me that almost all gophers once migrated to the Go world from other worlds. Basically, these are the worlds of hardcore OOP. There are very few people who started their journey in IT with the Go language, I have never even met such people. This is understandable: the language appeared relatively recently, and hypanul generally seems like yesterday.
However, there is a certain category of developers who subconsciously would like to plunge into Go, but due to their own beliefs, do not yet do this. Or they do something in Go partially, some small pieces of logic that their cozy PHP/Python/etc do not pull so well for some reason. In general, “Go is not for everything,” they say.

I was lucky: when moving into the world of Go, I ended up on a team obsessed with DDD and experiments related to bringing best practices from other languages. In a way, I’ve been able to get great pleasure from combining the strengths of Go itself and the positive experience that I had previously gained in other programming languages.

With DDD, you have all the elements and pieces of logic strictly in their place, while you think more in terms of business, and not just technical features. You get a well maintained, extensible and beautifully written project. But due to the peculiarities of the Go language, increased development discipline is required. Such discipline, in general, is required in other languages. The DDD++ language has not yet been invented, which is 100% tailored for everything that is in DDD, and makes it impossible to deviate from certain rules.

I’m already silent about the fact that in Go, in order to realize your domain driven ambitions, in some places it may be necessary to deviate from the official style guides of the Go creators. Remember: you are the creator here, and the programming language is just a tool.

Example: division into layers and their isolation from each other. My experience

There are already articles on the Internet about how in Go projects arrange the files into folders so that it turns out DDD. I want to share my experience a little, how it turned out for me, with a few examples.

Suppose we are making an application – a payment service. Let the application have the following layers:

  • applied (application)
  • subject area (domain)
  • infrastructure (infrastructure)

We lay out all our objects according to the Go-packages of the same name. And then the dilemma: how can we organize everything so that the details of the implementation of each layer do not stick out? Most often, it is recommended to name non-public objects with a small letter. But I liked another idea: put all structures that hide implementation details into a subpackage internal. This is a package whose contents are available to neighboring packages and the parent package, but not to everyone else. Let the layer package itself contain interfaces, as well as those types of objects that we will not hide (for example, entities, value objects, etc.). This leads to the idea that factory functions (which NewFooBar()) hidden in internal services can be placed in your subpackage factory.

Here’s what it might look like on an example layer domain:

- domain/
  - factory/
      user_repository_factory.go
      transaction_repository_factory.go
  - entity/
      user.go
      transaction.go
  - value/
      money.go
  - internal/
      user_repository.go
      transaction_repository.go
  user_repository_interface.go
  transaction_repository_interface.go

So, in our domain there are 2 entities: User and Transaction. For pulling out from the database and saving to the database (or to some other type of storage), they are responsible respectively UserRepository and TransactionRepository. Let’s describe the interfaces of these repositories and put them in the corresponding files in the root of the layer-package domain:

type UserRepositoryInterface interface {
    Get(id int64) (entity.User, error)
    FindByEmail(email string) (*entity.User, error)
    Save(user entity.User) error
}
type TransactionRepositoryInterface interface {
    Get(id int64) (entity.Transaction, error)
    FindByUserId(userId int64) (*entity.Transaction, error)
    Save(transaction entity.Transaction) error
}

Accordingly, in factory there will be such factories (for simplicity of illustration, we will omit dependency forwarding and other things):

func NewUserRepository() domain.UserRepositoryInterface {
    return &internal.UserRepository{}
}
func NewTransactionRepository() domain.TransactionRepositoryInterface {
    return &internal.TransactionRepository{}
}

AT internal implementations will lie UserRepository and TransactionRepository.
If we do not have a domain layer, but, for example, a service one, then in its internal implementations of services would leave.

But back to the domain layer. An inquisitive reader may object that the domain should not have any dependencies, which means that specific implementations of the repositories should be in the infrastructure layer. Well, no problem, move the two folders and get the following package structure:

- infrastructure/
  - factory/
      user_repository_factory.go
      transaction_repository_factory.go
  - internal/
      user_repository.go
      transaction_repository.go
- domain/
  - entity/
      user.go
      transaction.go
  - value/
      money.go
  user_repository_interface.go
  transaction_repository_interface.go

Example: Entity Behavior

In the case of entities, things are a little worse, but still acceptable. The fact is that an entity is not just a structure with fields that can be set as much and whenever you like. When working with entities, it is important to ensure that invariants – such business rules that force the possible states of an entity, and which cannot be observed only by technical restrictions in the form of strong typing, etc.

For example, in our payment service we have an entity called Transaction with the following fields (I will leave only those that are needed in this example):

type Transaction struct {
    ...
    Status string
    ProcessedAt *time.Time
}

Status — the current status of the transaction (can take values Created, Processing, Success, Failed). BUT ProcessedAt – the time of the transaction on the side of the external payment system, may be nilif the transaction has not yet been completed (has a status different from Success). If the transaction is in status Successthen the field ProcessedAt must necessarily have some meaning (i.e. not nil).

It turns out that if we allow to write values ​​​​in the fields of the transaction as we want, then the invariant with ProcessedAt and Status may not be observed at some point in time – that is, at some point in time, the entity Transaction may be in an invalid state.

It turns out that we still have to come up with something to change the state strictly through method calls that encapsulate the logic for checking the possibility of this change.
Let’s do this:

func (t *Transaction) SetSuccess(processedAt time.Time) error {
    if t.Status != "Processing" {
        return fmt.Errorf("cannot set success status after %v", t.Status)
    }

    t.Status = "Success"
    t.ProcessedAt = &processedAt

    return nil
}

In this case, the method SetSuccess guarantees us the correct transition from status to status, and also guarantees that ProcessedAt will be set at the same time as setting the successful status.

Okay, done beautiful methods. But the fields are still public … And here I offer you a choice of 3 options that you can do:

  1. Make fields “private”. But in this case, they will still be visible inside the package from other objects, and you will have to spawn a bunch of getters, which, it turns out, does not go way.
  2. Put each individual entity in its own separate package inside entity. The result is a “package with packages” and an overly complicated folder tree in the project.
  3. To score and agree with the whole team that the fields are not directly set, the state is changed strictly through methods, and violators of such law and order are beaten on the hands of the code review.

Declarative style of business logic description

The declarative description of business rules in Go turned out to be generally acceptable for me, aka the “specification” pattern, although in my case it is not 100% bookish. Here I will show one of the possible implementation examples, and I am sure that you will do better, prettier and more canonical.

Let’s imagine: in our payment service, it became necessary to create a small and initially not very complex component called “anti-fraud”. This component should allow or prohibit different users from replenishing the balance or withdrawing funds according to certain rules. For each payment system we support, this set of rules is different; also, the set of rules varies depending on the jurisdiction in which our service operates. At first, there are few such rules, but the business appetite comes with eating, and more and more new requirements appear, it becomes necessary to combine the rules with each other. And it is very important: our code should not turn into noodles.

So, let’s set the interface that will have to implement each rule of our antifraud:

type AntifraudRule interface {
    IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error)
    IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error)
    And(other AntifraudRule) AntifraudRule
    Or(other AntifraudRule) AntifraudRule
    AndNot(other AntifraudRule) AntifraudRule
}

The first two methods should check if the user should be allowed user deposit/withdraw amount money to/from wallet wallet in an external payment system. And the methods And(), Or(), AndNot() are operator methods, thanks to which we can build our rules into unique combinations.

Here is an example implementation of one of the rules. Let’s say we want to allow everyone to make deposits only on the growing moon, and payouts on the full moon. Then we write the following code:

type MoonRule struct {}

func NewMoonRule() AntifraudRule {
    return &MoonRule{}
}

func (r MoonRule) IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    if IsRisingMoon() {
        return true, nil
    }
    return false, nil
}

func (r MoonRule) IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    if IsFullMoon() {
        return true, nil
    }
    return false, nil
}

func (r MoonRule) And(other AntifraudRule) AntifraudRule {
    return NewAndRule(r, other)
}

func (r MoonRule) Or(other AntifraudRule) AntifraudRule {
    return NewOrRule(r, other)
}

func (r MoonRule) AndNot(other AntifraudRule) AntifraudRule {
    return NewAndNotRule(r, other)
}

In the last three methods we see the creation of instances AndRule, OrRule and AndNotRule. Here is what the implementation looks like AndRule:

type AndRule struct {
    left AntifraudRule
    right AntifraudRule
}

func NewAndRule(left AntifraudRule, right AntifraudRule) AntifraudRule {
    return &AndRule{
        left: left,
        right: right,
    }
}

func (r AndRule) IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    leftResult, err := r.left.IsDepositAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }
    rightResult, err := r.right.IsDepositAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }

    return leftResult && rightResult, nil
}

func (r AndRule) IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    leftResult, err := r.left.IsPayoutAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }
    rightResult, err := r.right.IsPayoutAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }

    return leftResult && rightResult, nil
}

func (r AndRule) And(other AntifraudRule) AntifraudRule {
    return NewAndRule(r, other)
}

func (r AndRule) Or(other AntifraudRule) AntifraudRule {
    return NewOrRule(r, other)
}

func (r AndRule) AndNot(other AntifraudRule) AntifraudRule {
    return NewAndNotRule(r, other)
}

Implemented in a similar way OrRule and AndNotRule.

And finally, how to use all this. Let’s say we have 3 rules: MoonRule, SunRule and RetrogradeMercuryRule. Let’s define that in the current version of our service we want to allow payments to people when we are favored: Moon And Sun OR Mercury retrograde. Let’s write the assembly of our antifraud with these conditions:

func NewAntifraud() AntifraudRule {
    moon := NewMoonRule()
    sun := NewSunRule()
    retrogradeMercury := NewRetrogradeMercuryRule()

    return moon.And(sun).Or(retrogradeMercury)
}

As you can see, it seems to work. And it seems that even Go didn’t really put a spoke in the wheels for us, and even annoying if err != nil hardly got in the way. And if the whole thing is thought out, combed, wow … As they say, there is no limit to perfection.

Instead of a conclusion

Despite the fact that DDD in Go is implemented with a certain number of compromises, I still saw more pluses from such an implementation than minuses. Yes, somewhere you have to close your eyes to something, somewhere to dodge. But even as it is, it’s worth it. And certainly I would not call it “stretching an owl on a globe.”

Similar Posts

Leave a Reply Cancel reply