The ddd Hamburger for Go

The DDD Hamburger is my favorite type of architecture for Go. Why? The DDD Hamburger beautifully combines the best of the hexagonal architecture of Domain-Driven-Design and layered architecture. Get to know the DDD Hamburger Architecture for Go and maybe it’ll become your favorite too!

The DDD Hamburger Overview 🍔

DDD Hamburger Overview

The DDD Hamburger is a real Hamburger with well defined layers from top to bottom:

The upper Bun half 🍞
The upper bun half is the top of the Hamburger and is the presentation layer. The presentation layer contains your REST API and the web interface.

The Salad 🥗
Below the bun is the salad which is the application layer. The application layer contains the use case logic of your application.

The Meat 🥩
Next is the meat of your Hamburger, the domain layer. Just like the meat the domain layer is the most important part of your Hamburger. The domain layer contains you domain logic and all entities, aggregates and value objects of your domain.

The lower Bun half 🍞
The lower bun half is the infrastructure layer. The infrastructure layer contains concrete implementations of your repositories for a Postgres database. The infrastructure layer implements interfaces declared in the domain layer.

Got the idea? Great, so let’s look into the details.

Go Example of the DDD Hamburger

Now we’ll code a Go application using our DDD Hamburger. We’ll use a simple time tracking example application with activities to show the practical Go implementation. New activities are added using a REST API and are then stored in a Postgres database.

The DDD Hamburger Architecture applied to Go is shown below. We’ll go through all layers of the Hamburger in detail soon.

details|500

The Presentation Layer as upper Bun 🍞

The presentation layer contains the HTTP handlers for the REST API. The HTTP handlers like HandleCreateActivity create simple handler functions. All handlers for activities hang off a single struct ActivityRestHandlers, as the code below shows.

type ActivityRestHandlers struct {
    actitivityService  *ActitivityService
}
 
func (a ActivityRestHandlers) HandleCreateActivity() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // ...
    }
}

The actual logic to create a new activity is handled by a service
ActivityService of the underlying application layer.

The HTTP handlers don’t use the activity entity of the domain layer as JSON representation. They use their own model for JSON which contains the struct tags to serialize it to JSON properly, as you see below.

type activityModel struct {
    ID          string        `json:"id"`
    Start       string        `json:"start"`
    End         string        `json:"end"`
    Duration    durationModel `json:"duration"`
}

That way our presentation layer only depends on the application layer and the domain layer, not more. We allow relaxed layers, which means it’s ok to skip the application layer and use stuff from the domain layer directly.

The Application Layer as Salad 🥗

The application layer contains service which implement the use cases of our application. One use case is to create an activity. This use case is implemented in the method CreateActivity that hangs of the application service struct ActitivityService.

type ActitivityService struct {
    repository ActivityRepository
}
 
func (a ActitivityService) CreateActivity(ctx context.Context, activity *Activity) (*Activity, error) {
    savedActivity, err := a.repository.InsertActivity(ctx, activity)
    // ... do more
    return savedActivity, nil
}

The application services use the repository interface ActivityRepository for the use case. Yet it only knows the interface of the repository which is declared in the domain layer. The actual implementation of that interface is not of concern to the application layer.

The application services also handle the transaction boundaries, since one use case shall be handled in one single atomic transaction. E.g. the use case of creating a new project with an initial activity for it has to go through in one transaction, although it’ll use one repository for activities and one for projects.

The Domain Layer as Meat 🥩

The most important part is the domain layer which is the meat of our DDD hamburger. The domain layer contains the domain entity Activity, the logic of the domain like calculating the activity’s duration and the interface of the activity repository.

// Activity Entity
type Activity struct {
    ID             uuid.UUID
    Start          time.Time
    End            time.Time
    Description    string
    ProjectID      uuid.UUID
}
 
// -- Domain Logic
// DurationDecimal is the activity duration as decimal (e.g. 0.75)
func (a *Activity) DurationDecimal() float64 {
    return a.duration().Minutes() / 60.0
}
 
// ActivityRepository
type ActivityRepository interface {
    InsertActivity(ctx context.Context, activity *Activity) (*Activity, error)
    // ... lot's more
}

The domain layer is the only layer that’s not allowed to depend on other layers. It should also be implemented using mostly the Go standard library. That’s why we neither use struct tags for json nor any database access code.

The Infrastructure Layer as lower Bun 🍞

The infrastructure layer contains the concrete implementation of the repository domain interface ActivityRepository in the struct DbActivityRepository. This repository implementation uses the Postgres driver pgx and plain SQL to store the activity in the database. It uses the database transaction from the context, since the transaction was initiated by the application service.

// DbActivityRepository is a repository for a SQL database
type DbActivityRepository struct {
    connPool *pgxpool.Pool
}
 
func (r *DbActivityRepository) InsertActivity(ctx context.Context, activity *Activity) (*Activity, error) {
    tx := ctx.Value(shared.ContextKeyTx).(pgx.Tx)
    _, err := tx.Exec(
        ctx,
        `INSERT INTO activities 
           (activity_id, start_time, end_time, description, project_id) 
         VALUES 
           ($1, $2, $3, $4, $5)`,
        activity.ID,
        activity.Start,
        activity.End,
        activity.Description,
        activity.ProjectID,
    )
    if err != nil {
        return nil, err
    }
    return activity, nil
}

The infrastructure layer depends on the domain layer and may use all entities, aggregates and repository interfaces from the domain layer. But from the domain layer only.

Assemble the Burger in the Main Function

No we have meat, salad and bun lying before us as single pieces. It’s time to make a proper Hamburger out of these pieces. We assemble our Hamburger in the main function, as you see below.

To wire the dependencies correctly together we work from bottom to top:

  1. First we create a new instance of of the db activity repository DbActivityRepository and pass in the database connection pool.
  2. Next we create the application service ActivityService and pass in the repository.
  3. Now we create the ActivityRestHandlers and pass in the application services. We now register the HTTP handler functions with the HTTP router.

The code to assemble our DDD Hamburger architecture is like this:

func main() {
    // ...
 
    // Infrastructure Layer with concrete repository
    repository := tracking.NewDbActivityRepository(connectionPool)
 
    // Application Layer with service
    appService := tracking.NewActivityService(repository)
 
    // Presentation Layer with handlers
    restHandlers := tracking.NewActivityRestHandlers(appService)
    router.HandleFunc("/api/activity", restHandlers.HandleCreateActivity())
}

The code to assemble our Hamburger is plain, simple and very easy to understand. I like that, and it’s all I usually need.

Package Structure for the DDD Hamburger

One question remains: Which structure of our Go packages is best suited for the DDD Hamburger?

I usually start out with a single package for all layers. So a single package tracking with files activity_rest.go for the rest handlers, activity_service.go for application services, the domain layer in activity_domain.go and the database repository in activity_repository_db.go.

A next step is to separate out all layers as separate packages, except the domain layer. So we’d have a root package tracking. The root package contains the domain layer. The root package has sub package for each layer like application, infrastructure, presentation. Why the domain layer in the root package? That way we can use the domain layer with it’s proper name. So if we’d use the domain entity Activity somewhere, the code would read tracking.Activity which is very nice to read.

Whichever package structure is best depends on your project. I’d suggest you start small and easy and adjust it as your project grows over time.

Wrap Up of the DDD Hamburger 🍔

The DDD Hamburger is a layered architecture based fully on Domain Driven Design. It’s very easy to understand and follow. That’s why the DDD Hamburger is my favorite architectural style. In general and especially in Go.

Using the DDD Hamburger for your Go application is pretty straightforward, as you’ve seen. You can start out small yet you’re able to grow as needed.

See an application of the DDD Hamburger below:

GitHub logo Baralga / baralga-app

Credits

The DDD Hamburger Architecture was coined by Henning Schwentner, so thanks for that. Another great influence for structuring the HTTP handlers came from Mat Ryer.