MiniCart
Stage 01 Hello, Hexagon

Stage 01 - Hello, Hexagon

Understand the problem hexagonal architecture solves, then get a server running.

Prereqs: stage 00 complete · Time: ~20 min

By the end: curl localhost:8080/health returns {"status":"ok"}.


Why hexagonal

Goal: understand the specific problem hexagonal architecture solves.

Learn

Consider a handler written the "just make it work" way - one function doing everything:

Go
func handleCreateProduct(w http.ResponseWriter, r *http.Request) {
	var body map[string]any
	json.NewDecoder(r.Body).Decode(&body)               // HTTP concern
	if body["price"].(float64) <= 0 {                    // business rule
		w.WriteHeader(400)
		return
	}
	db.Exec("INSERT INTO products VALUES ($1, $2)", ...) // database concern
	w.WriteHeader(201)                                   // HTTP concern
}

You will never write this. Notice only that three unrelated jobs - parsing HTTP, a business rule, and SQL - share one function body. That causes concrete problems:

  • You can't test the price rule without starting an HTTP server and a database.
  • You can't change the database without editing code that also does HTTP.
  • You can't reuse the price rule elsewhere without copying it.

The business rule - the one line that makes this your application - is trapped between JSON parsing and a SQL string. It has no place of its own.

Hexagonal architecture (Alistair Cockburn's term; also "ports & adapters") gives it one:

Put the business logic in the centre. Make everything that touches the outside world - HTTP, databases, queues - a replaceable adapter around it.

The centre never imports the adapters. The adapters depend on the centre. The next section states that as a rule and draws the map.

Exercise

In the handler above, which line is business logic, and roughly what fraction of the function is it?

Answer

One line: if price <= 0 { reject }. Perhaps 10% of the function. The other 90% is interchangeable plumbing - but as written, the 10% can't be tested or reused without dragging the 90% along. Hexagonal architecture keeps the two apart.


The Dependency Rule

Goal: learn the four layers and the rule that orders them.

Learn

The code is organised in four layers. Each may import only layers closer to the centre:

Code
presentation/     input adapters   - HTTP handlers, later Kafka consumers
      │ imports
      ▼
application/      use cases        - orchestrates the work
      │ imports
      ▼
domain/           entities + ports - business rules; defines the interfaces
      ▲
      │ implements the ports
infrastructure/   output adapters  - storage, message producers

The Dependency Rule:

Source-code dependencies point inward. presentation → application → domain. Infrastructure implements interfaces the domain defines; the domain imports nothing outward.

Read the arrows as "is allowed to import." Three point down toward the domain. The fourth, from infrastructure/, points up - because infrastructure implements a port (an interface) the domain owns, so it depends on the domain, not the reverse.

Two consequences you'll rely on for the rest of the course:

  • The domain has no imports from HTTP or a database, so you can test it with no server and no DB.
  • Swapping one output adapter for another (in-memory → Postgres) touches no business code, because the business code depends on the port, not the adapter.

You don't need to memorise this. You'll build each layer in order, from the domain outward, over stages 02–05.

Where main.go fits

None of the four layers. main.go is the composition root - the edge of the program where all the layers get assembled and switched on. It is allowed to import everything, because something has to plug the pieces together. You build a minimal one next.

Exercise

If catalog/infrastructure/postgres implements catalog/domain.Repository, which package imports which?

Answer

The Postgres adapter imports catalog/domain. The domain defines the interface and knows nothing about Postgres. That is dependency inversion: the detail depends on the policy, not the other way around.


The health server

Goal: a running server that answers GET /health.

Build

1. In your minimart module, create main.go:

Go
package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	})

	log.Println("minimart listening on http://localhost:8080")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}
Go note: since Go 1.22 the standard router reads the HTTP method as part of the pattern ("GET /health"), so no framework is needed to route by method.

2. Run it:

Shell
go run .

main.go stays this small for a while. It's the composition root; the health endpoint lives here - not in a domain - because "is this process up?" is an operational question, not a business capability. It grows into a module-loading boot sequence in stage 07.

Checkpoint

In a second terminal:

Shell
curl http://localhost:8080/health
# {"status":"ok"}

curl -i http://localhost:8080/nope | head -1
# HTTP/1.1 404 Not Found

The prompt may sit right after } - the response has no trailing newline, which is fine.

What you learned

  • The Dependency Rule: imports point inward, toward the domain.
  • main.go is the composition root - it wires things together and owns operational endpoints like /health.
  • Go's net/http routes by method and path with no framework.

You won't touch this server again until stage 05. The next three stages build the domain, application, and storage - all runnable with no network at all.

Exercise

Why does /health belong in main.go instead of the future catalog domain?

Answer

Health is an operational question: is this process alive and able to answer? It is not a catalog rule. Keeping it in the composition root prevents a business package from collecting deployment concerns too early.