DEV Community

Cover image for The DDD Hamburger for Go
Jan Stamer
Jan Stamer

Posted on • Updated on

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.

Go Example of the DDD Hamburger

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) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

The application services uses 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 an 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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

Simple and lightweight time tracking for individuals and teams, for the cloud in the cloud.

Baralga

FOSSA Status

Multi user time tracking application with web frontend and API.

User Guide

Keyboard Shortcuts

Track Activities

Shortcut Action
Alt + Shift + n Add Activity
Alt + Shift + p Manage Projects

Report Activities

Shortcut Action
Shift + Arrow Left Show previous Timespan
Shift + Arrow Down Show current Timespan
Shift + Arrow Right Show next Timespan

Administration

Accessing the Web User Interface

The web user interface is available at http://localhost:8080/. You can log in as administrator with admin/adm1n or as user with user1/us3r.

Configuration

The backend is configured using the following environment variables:

Environment Variable Default Value Description
BARALGA_DB postgres://postgres:postgres@localhost:5432/baralga PostgreSQL Connection string for database
BARALGA_DBMAXCONNS 3 Maximum number of database connections in pool.
PORT 8080 http server port
BARALGA_WEBROOT http://localhost:8080 Web server root
BARALGA_JWTSECRET secret Random secret for JWT generation
BARALGA_CSRFSECRET CSRFsecret Random secret for CSRF protection
BARALGA_ENV dev use production for production mode
BARALGA_SMTPSERVERNAME

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.

Top comments (7)

Collapse
 
proteusiq profile image
Prayson Wilfred Daniel

Now, I am hungry 🤤. Thank you for the creativeness. We did something similar withDDD Architecture but with Python(ML) + Rust(Backend API) + React(Frontend) as languages.

Collapse
 
raguay profile image
Richard Guay • Edited

I guess it goes good with Bun (the JavaScript runtime)! 😉

Collapse
 
jangelodev profile image
João Angelo

Great Jan Stamer

Collapse
 
nxquan profile image
nxquan

I like you!

Collapse
 
nxquan profile image
nxquan

I like it!

Collapse
 
joaopinheiro profile image
Joao Pinheiro

You are aware that this is basically three-tier application design and a variant of onion architecture, right? Something that actually exists and it has now been used for decades. And none of this is actually DDD - having a domain layer service isn't per se DDD. Look, I actually not only recommend, but use this approach (or variations of it) in several languages over the past 15 years - including go; but let me be blunt - not only this is not a new thing and already has a name, but anyone who is not familiarized with these architectural patterns (or think they are somewhat new) should not be designing applications.

Collapse
 
remast profile image
Jan Stamer

Fair enough. Yet my perception is that it's not that widely known. So I wanted to spread the love for that kind of architecture. If you're already aware and using it, that's great!

Some comments have been hidden by the post's author - find out more