Project Structure — Go Package Layout for a Workflow Engine
Building a Distributed Workflow Engine·Part 3 of 11·4 min read

Project Structure — Go Package Layout for a Workflow Engine

On this page · 6 sections
  1. Step 1: Initialize the Go module
  2. Step 2: Create the directory layout
  3. Step 3: Understand the layers
  4. Step 4: Add the dependencies
  5. Step 5: The processes you will run
  6. Step 6: The build commands

Now set up the project. This page creates the Go module, lays out the packages, and explains which package owns which job. Keep this page open as a map while you build the rest of the chapter.

Step 1: Initialize the Go module

mkdir composer && cd composer
go mod init github.com/mohitkumar/composer

Step 2: Create the directory layout

mkdir -p cmd/{coordinator,worker,language-server-py,language-server-js}
mkdir -p pkg/{model,statemachine,coord,template,lease,outbox,redisq,db,auth,util}
mkdir -p pkg/stream/redis pkg/api/{http,grpc,proto}
mkdir -p worker/executors migrations infra tests docs/diagrams benchmark web

The finished tree looks like this:

composer/
├── cmd/
│   ├── coordinator/          coordinator binary (flags, bootstrap)
│   ├── worker/               worker binary (queues, streams, concurrency)
│   ├── language-server-py/   Python sandbox server (RestrictedPython + audit hooks)
│   └── language-server-js/   JavaScript sandbox server (isolated-vm)
├── pkg/
│   ├── model/                definitions, DAG index, run context, statuses
│   ├── statemachine/         the workflow state machine: events, state, actions (pure)
│   ├── coord/                coordinator shell: engine, persister, repository, definitions, runs
│   ├── template/             CEL template resolver
│   ├── lease/                task leases
│   ├── outbox/               outbox enqueuer (LISTEN/NOTIFY + polling)
│   ├── stream/               stream abstraction, queue/shard naming
│   │   └── redis/            Redis Streams implementation
│   ├── redisq/               thin Redis client wrapper
│   ├── db/                   pgx pool + DB / Querier interfaces
│   ├── api/
│   │   ├── http/             REST API (chi)
│   │   ├── grpc/             worker-facing gRPC service
│   │   └── proto/            .proto files + generated code
│   ├── auth/                 JWT login
│   └── util/                 OpenTelemetry metrics and traces
├── worker/                   worker runtime: readers, executor pool, executors, coordinator client
│   └── executors/            HTTP executor, Executor interface
├── migrations/               database schema (partitions, triggers, seed data)
├── tests/                    end-to-end tests against real PostgreSQL and Redis
├── infra/                    Docker Compose, Helm chart, bootstrap scripts
├── web/                      Next.js web UI
└── benchmark/                k6 load tests

Step 3: Understand the layers

The packages form layers. Each layer only imports layers below it:

Composer's package layers from binaries at the top down to pkg/model and pkg/template at the bottom; each layer imports only layers below it cmd/coordinator · cmd/workerbinaries, flags, wiringpkg/api/http · pkg/api/grpctransport for clients and workerspkg/coordengine · persister · repositoryworker/readers · executors · routerpkg/outboxpkg/leasepkg/streamworker/executorspkg/statemachinefunctional core · no I/Opkg/model · pkg/templatedata types · DAG · CELimportsonlydown
Each layer imports only the layers below it

Two rules keep this clean:

  1. pkg/statemachine never imports pkg/coord, pgx or Redis. It only knows about pkg/model and two tiny interfaces (Store, TemplateResolver) that the shell implements.
  2. The worker never touches PostgreSQL. It talks to the coordinator over gRPC and to Redis for messages. Only the coordinator owns the database.

Step 4: Add the dependencies

go get github.com/jackc/pgx/v5            # PostgreSQL driver and pool
go get github.com/redis/go-redis/v9       # Redis Streams
go get github.com/google/cel-go           # CEL expressions in templates and conditions
go get github.com/google/uuid             # UUIDv7 run and task IDs
go get google.golang.org/grpc google.golang.org/protobuf
go get github.com/go-chi/chi/v5           # HTTP router
go get github.com/rs/zerolog              # structured logging
go get github.com/spf13/cobra github.com/spf13/viper   # flags + env config
go get github.com/Yiling-J/theine-go      # definition cache
go get go.opentelemetry.io/otel           # metrics and traces
go get github.com/testcontainers/testcontainers-go     # real Postgres/Redis in tests
Dependency Why this one
pgx/v5 Fast native PostgreSQL driver. You need LISTEN/NOTIFY, which pgx exposes directly through WaitForNotification.
go-redis/v9 Full support for Streams: XADD, XREADGROUP, XAUTOCLAIM, XPENDING.
cel-go Google's Common Expression Language. Safe (no loops, no I/O), fast, and familiar to anyone who wrote Kubernetes policies.
uuid uuid.NewV7() gives IDs that sort by creation time — you will use that to partition tables by run ID.
theine-go A W-TinyLFU cache. A few hot workflows get most of the traffic; this policy keeps them cached.
testcontainers-go End-to-end tests start real PostgreSQL and Redis containers.

Step 5: The processes you will run

Composer is not one binary. It is a small set of processes, each scaled independently:

Coordinators own PostgreSQL and publish to Redis; workers read Redis, call the coordinator over gRPC and send code to language servers coordinator × N:8080 REST · :50051 gRPCworker × MWORKER_QUEUES=…language serverspython :50053 · js :50052PostgreSQLsource of truthRedistask streamsSQLpublishreadgRPCExecute
Five kinds of process, each scaled on its own
Process Scales by
Coordinator Adding replicas. It is stateless; the run row lock serializes work per run.
Worker Adding replicas or raising WORKER_CONCURRENCY.
Language servers Independently of workers. User code runs here, in sandboxes.
PostgreSQL Partitioning keeps hot indexes small; retention drops whole months.
Redis Shards per queue (--stream_shards), so one queue spreads across Redis Cluster nodes.

Step 6: The build commands

The Makefile wraps everything you need:

make install-deps   # Go modules, npm and uv dependencies
make proto          # generate gRPC code (Go, Python)
make build          # bin/coordinator, bin/worker, language servers, web UI
make test           # unit + container-backed tests (needs Docker)
make docker-up      # the whole stack in Docker Compose

You now have a skeleton. The next page fills in the bottom layer: the workflow model.