Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang and DDD domain modeling

I've been studying domain-driven design lately and must say this type of architectural design triggers something in me. When I try to apply its concepts to my Go project I've encountered some obstacles. Following are some example methods, but I'm very uncertain which method to GO with.

Excerpt of the project structure:

├── api/
├── cmd/
├── internal/
|   ├── base/
|   |   ├── eid.go
|   |   ├── entity.go
|   |   └── value_object.go
|   ├── modules/
|   |   ├── realm/
|   |   |   ├── api/
|   |   |   ├── domain/
|   |   |   |   ├── realm/
|   |   |   |   |   ├── service/
|   |   |   |   |   ├── friendly_name.go
|   |   |   |   |   ├── realm.go
|   |   |   |   |   └── realm_test.go
|   |   |   |   └── other_subdomain/
|   |   |   └── repository/
|   |   |       ├── inmem/
|   |   |       └── postgres/

Common for all methods:

package realm // import "git.int.xxxx.no/go/xxxx/internal/modules/realm/domain/realm"

// base contains common elements used by all modules
import "git.int.xxxx.no/go/xxxx/internal/base"

Method #1:

type Realm struct {
   base.Entity

   FriendlyName FriendlyName
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
   var err error
   var r = new(Realm)

   r.Entity = base.NewEntity(id)
   r.FriendlyName, err = NewFriendlyName(params.FriendlyName)

   return r, err
}

type FriendlyName struct {
    value string
}

var ErrInvalidFriendlyName = errors.New("invalid friendly name")

func (n FriendlyName) String() string { return n.value }

func NewFriendlyName(input string) (FriendlyName, error) {
    if input == "" {
        return ErrInvalidFriendlyName
    }
    // perhaps some regexp rule here...

    return FriendlyName{value: input}, nil
}

With this method I think there will be much repeated code in the long run, but at least the FriendlyName value-object is immutable as per DDD requirements and opens up for more methods to be attached.

Method #2:

type Realm struct {
    base.Entity

    FriendlyName string
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
    var err error

    if err = validateFriendlyName(params.FriendlyName); err != nil {
        return nil, err
    }

    entity := base.NewEntity(id)

    return &Realm{
        Entity: entity,
        FriendlyName: params.FriendlyName,
    }, nil
}

This must be the most common one I've come across examples out there, except for the validation that very many examples lack.

Method #3:

type Realm struct {
    base.Entity

    friendlyName string
}

type CreateRealmParams struct {
    FriendlyName string
}

func CreateRealm(id base.EID, params *CreateRealmParams) (*Realm, error) {
    var err error

    if err = validateFriendlyName(friendlyName); err != nil {
        return nil, err
    }

    entity := base.NewEntity(id)

    return &Realm{
        Entity: entity,
        friendlyName: friendlyName,
    }, nil
}

func (r *Realm) FriendlyName() string { return r.friendlyName }
func (r *Realm) SetFriendlyName(input string) error {
    if err := validateFriendlyName(input); err != nil {
        return err
    }
    r.friendlyName = input
    return nil
}

Here the friendly name type is just a string, but immutable. This structure reminds me of Java code... When looking up a realm, should the repository-layer use the setter methods from the domain model to construct the realm aggregate? I tried with a DTO implementation placed in the same package (dto_sql.go) that encoded/decoded to/from the realm aggregate, but it kind of felt wrong having that concern placed in the domain package.

If you are facing the same issues as me, know about any other method or have anything to point out, I'll be very interested in hearing from you!

like image 211
Karl Andresen Avatar asked Feb 25 '19 20:02

Karl Andresen


1 Answers

First of all, as other commenters rightfully say, you have to look at the goals of DDD and decide whether the approach has merit. DDD adds some complexity to the architecture (most of that in the initial phase when structuring your project and base types) and the amount of boilerplate and ceremony you'll have to deal with after that.

In many cases simpler designs, e.g. a CRUD approach, work best. Where DDD shines is in applications that are in themself more complex in terms of functionality and/or where the amount of features is expected to significantly grow over time. Technical advantages can come in terms of modularity, extensibility and testability, but - most importantly imho - providing a process in which you can take the non-technical stakeholders along and translate their wishes to code without losing them along the way.

There's a great series of blog posts, the Wild Workouts Go DDD Example, that takes you along a refactoring process of a traditional Go CRUD-based REST API design to a full-blown DDD architecture, in several steps.

Robert Laszczak, author of the series defines DDD as follows:

Ensure that you solve valid problems in the optimal way. After that implement the solution in a way that your business will understand without any extra translation from technical language needed.

And he sees Golang + DDD as the excellent way to write business applications.

Key to understand here is to decide how far you want to go (no pun intended) with your design. The refactoring gradually introduces new architecture concepts, and at each of these steps you should decide if it is enough for your use case, weigh the pros and cons to go further. They start very KISS with a DDD Lite version, and then later go further with CQRS, Clean Architecture, Microservices and even Event Sourcing.

What I see in many projects is that they immediately go the Full Monty, creating overkill. Especially Microservices and Event Sourcing add a lot of (accidental) complexity.


I am not well-versed in Go yet (actually quite new to the language), but'll give a stab at your options and offer some considerations. Maybe more experienced Go developers can correct me, where I go off-the-mark :)

For my own project I am looking into a Clean Architecture (Ports & Adapters, Inversion of Control) + CQRS + DDD combination.

The Wild Workouts example provides ample inspiration, but will need some tweaks and additions here and there.

My goal is that in the codebase's folder structure developers should immediately recognize where features / use cases (epics, user stories, scenario's) reside, and have self-contained, fully consistent domains that directly reflect the Ubiquitous Language and are separately testable. Part of testing will be text-only BDD scripts that can be easily understood by customer and end-users.

There will be some boilerplate involved, but - given the above - I think the pros outweigh the cons (if your application warrants DDD).

Your Option #1 looks best to me, but with some additional observations (note: I will stick to your naming, which will make some of this seem overkill.. again it is the ideas that count).

  • Instead of Entity I would say that Realm represents an AggregateRoot.
  • This can be implicitly so, or it can inline a base.AggregateRoot.
  • The aggregate root is the access point to the domain, and ensures its state is always consistent.
  • Hence the internal state of Realm should be immutable. State changes occur via functions.
  • Unless really trivial and not likely to change I'd implement FriendlyName value object in a separate file.
  • Also part of the domain is a RealmRepository but this provides no more than an interface.

Now I'm using CQRS, which is an extension to what's shown in your code snippets. In this:

  • There might be a ChangeFriendlyName Command handler in the Application layer.
  • The handler has access to a repository implementation e.g. InMemRealmRepository.
  • Might pass a CreateRealmParams to the Command, which then does its validation.
  • Handler logic might start by fetching a Realm aggregate from the database.
  • Then constructs a new FriendlyName (can also encapsulate in a Realm function call).
  • A function call to Realm updates state and queues a FriendlyNameChanged event.
  • The command handler persists the changes to the InMemory database.
  • Only if there were no errors the command handler invokes Commit() on the aggregate.
  • One or more queued events are now published e.g via an EventBus, handled where needed.

As for the code of Option #1 some changes (hope I'm doing this right) ..

realm.go - Aggregate root

type Realm struct {
   base.AggregateRoot

   friendlyName FriendlyName
}

// Change state via function calls. Not shown: event impl, error handling.
// Even with CQRS having Events is entirely optional. You might implement
// it solely to e.g. maintain an audit log.
func (r *Realm) ChangeFriendlyName(name FriendlyName) {
   r.friendlyName = name
   
   var ev = NewFriendlyNameChanged(r.id, name)

   // Queue the event.
   r.Apply(ev)
}

// You might use Params types and encapsulate value object creation,
// but I'll pass value objects directly created in a command handler.
func CreateRealm(id base.AID, name FriendlyName) (*Realm, error) {
   ar := base.NewAggregateRoot(id)

   // Might do some param validation here.

   return &Realm{
       AggregateRoot: ar,
       friendlyName: name,
   }, nil
}

friendlyname.go - Value object

type FriendlyName struct {
    value string
}

// Domain error. Part of ubiquitous language.
var FriendlyNameInvalid = errors.New("invalid friendly name")

func (n FriendlyName) String() string { return n.value }

func NewFriendlyName(input string) (FriendlyName, error) {
    if input == "" {
        return FriendlyNameInvalid
    }
    // perhaps some regexp rule here...

    return FriendlyName{value: input}, nil
}
like image 128
Arnold Schrijver Avatar answered Nov 19 '22 16:11

Arnold Schrijver