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 π
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.
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 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
}
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:
- First we create a new instance of of the db activity repository
DbActivityRepository
and pass in the database connection pool. - Next we create the application service
ActivityService
and pass in the repository. - 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:
Baralga / baralga-app
Simple and lightweight time tracking for individuals and teams, for the cloud in the cloud.
Baralga
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)
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.
I guess it goes good with Bun (the JavaScript runtime)! π
Great Jan Stamer
I like you!
I like it!
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.
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