| title | Doc-First Development: Because 'We'll Document It Later' Is a Lie |
|---|---|
| published | true |
| description | After 20+ years in software, I've seen every approach. Spec-first is the one that actually works. Here's how. |
| tags | go, testing, openapi, architecture |
| cover_image | https://raw.githubusercontent.com/copyleftdev/synapse-spec-first/main/media/logo.png |
| canonical_url | https://github.com/copyleftdev/synapse-spec-first |
After 20+ years in software, I've seen every approach to building systems. This is the one that actually works.
Every time I mention "doc-first" or "spec-driven development" to engineers, I get blank stares.
"That sounds great in a perfect world."
Here's the thing: We're software engineers. We create worlds. Why wouldn't we try to make them perfect?
This article—and the complete codebase on GitHub—exists to demonstrate that spec-first isn't some ivory tower ideal. It's practical. It's powerful. And with the tools we have today, it's easier than ever.
Synapse is a complete event-driven order processing system built entirely spec-first:
- Specifications are the source of truth — OpenAPI 3.1 for REST, AsyncAPI 3.0 for events
- Code is generated from specs — Types, interfaces, clients, event handlers
- Conformance tests validate everything — Implementation must match the specification
- Real infrastructure via Testcontainers — NATS, PostgreSQL, Redis spin up for tests
The result? A system where the contract is king, drift is impossible, and tests prove conformance—not just behavior.
Traditional development flows like this:
Write code → Document later (maybe) → Hope nothing drifts
Doc-first development flips the script:
Write spec → Generate code → Implement interfaces → Prove conformance
When code is generated from specs:
- ✅ No drift between documentation and implementation
- ✅ Type safety guaranteed by the spec
- ✅ Clients auto-generated for any language
- ✅ Changes flow spec → code, never backwards
Traditional testing asks: "Does the code do what the code says?"
Conformance testing asks: "Does the code do what the contract says?"
One tests implementation. The other tests promises.
Orders flow through three Watermill-powered stages:
| Stage | Purpose |
|---|---|
| Validate | Check required fields, verify amounts, validate customer |
| Enrich | Customer tier lookup, fraud scoring, inventory check |
| Route | Apply routing rules, determine destination, set priority |
Each stage publishes to NATS, persists to PostgreSQL, and caches in Redis. Failed events go to a Dead Letter Queue for retry.
func TestOpenAPI_HealthEndpoint_ConformsToSpec(t *testing.T) {
// Start real infrastructure with Testcontainers
tc, _ := testutil.StartContainers(ctx, t, nil)
// Create test suite from OpenAPI spec
suite, _ := conformance.NewContractTestSuite(
"openapi/openapi.yaml",
)
// Validate response matches spec schema
result := suite.RunTest(ctx, client, baseURL,
"GET", "/health",
nil,
http.StatusOK,
"HealthResponse", // Must match this schema
)
assert.True(t, result.Passed)
}func TestAsyncAPI_OrderPayload_ConformsToSpec(t *testing.T) {
// Create validator from AsyncAPI spec
suite, _ := conformance.NewEventContractTestSuite(
"asyncapi/asyncapi.yaml",
)
// Validate event payload against schema
result := suite.ValidateEvent(
"orders/ingest",
"OrderReceivedPayload",
orderJSON,
)
assert.True(t, result.Passed)
}synapse-spec-first/
├── asyncapi/ # AsyncAPI 3.0 event specs
├── openapi/ # OpenAPI 3.1 REST specs
├── cmd/
│ ├── synapse/ # Application entry point
│ └── synctl/ # Custom code generator
├── internal/
│ ├── generated/ # Generated from specs
│ │ ├── types.gen.go # 31 domain types
│ │ ├── server.gen.go # HTTP interface
│ │ ├── client.gen.go # HTTP client
│ │ └── events.gen.go # Event handlers
│ ├── handler/ # HTTP handlers
│ ├── pipeline/ # Watermill pipeline
│ ├── conformance/ # Contract testing
│ └── testutil/ # Testcontainers
└── scripts/ # Diagram generation
# 1. Edit the spec (the source of truth)
vim openapi/components/schemas/orders.yaml
# 2. Regenerate code
go run ./cmd/synctl
# 3. Implement the new interface methods
# (The compiler tells you what's missing)
# 4. Run conformance tests
go test ./internal/conformance/... -vThat's it. Spec changes → regenerate → implement → verify. The spec leads, the code follows.
This project wouldn't be possible without brilliant work from:
Specification Standards
- OpenAPI Initiative — Giving REST APIs a language
- AsyncAPI Initiative — The same for event-driven systems
- JSON Schema — The foundation for validation
Testing Infrastructure
- Testcontainers — Real integration testing made accessible
- Watermill — Elegant Go event-driven development
The Go Ecosystem
I've heard every objection:
"We don't have time to write specs first."
You don't have time to debug integration issues from undocumented API changes either. Pick your poison.
"Specs get out of date."
Not when they generate code. Not when conformance tests fail on drift.
"It's too much overhead."
The overhead is front-loaded. The payoff compounds forever.
# Clone the repo
git clone https://github.com/copyleftdev/synapse-spec-first.git
cd synapse-spec-first
# One-time setup
make setup
# Run all tests (including conformance)
make test
# Start the server
make runWe've included a comprehensive Makefile because developer experience matters:
make help # See all available commands
make generate # Regenerate code from specs
make test-conformance # Run contract tests
make test-pipeline # Run integration tests
make diagrams # Generate architecture diagrams
make dev # generate → test → run (full cycle)| Workflow | Command |
|---|---|
| First time setup | make setup |
| After spec changes | make generate |
| Quick validation | make test-short |
| Full test suite | make test |
| CI pipeline | make ci |
{% github copyleftdev/synapse-spec-first %}
If you've never tried spec-first development, I challenge you:
Build your next API starting with OpenAPI.
- Write the spec first
- Generate your types
- Implement to the interface
- Write conformance tests
Then tell me it's not worth it.
Someone once told me, "In a perfect world, that would be great."
We're software engineers. We create worlds.
Why wouldn't we try to make them perfect?
This project is a living demonstration. Fork it. Learn from it. Improve it. And maybe, just maybe, next time someone mentions doc-first development, there will be one fewer blank stare.
20 years of testing taught me this: Quality isn't an afterthought. It's the architecture.
Now go build something beautiful.
- Full Codebase: github.com/copyleftdev/synapse-spec-first
- OpenAPI Spec: openapi/openapi.yaml
- AsyncAPI Spec: asyncapi/asyncapi.yaml
- Conformance Tests: internal/conformance/




