Stage 7 · Master
Phase 1 — Project Foundation
Project Architecture
Before any Go file exists, decide what the HOA platform's services own, how they talk to each other, and what a monorepo with go.work buys the team over one sprawling module.
The Decisions That Outlive Every Line of Code
The repository we are about to create is empty. That is the best possible moment to make architectural decisions, because every choice made now is cheap to change and every choice made after the fifth service exists is expensive. This course builds a real HOA (Homeowners' Association) management platform: organizations (the societies themselves), residents, payments, complaints, visitors, and more will each become their own deployable service over the life of the course. Phase 1 exists to decide, once, how those services will be organized, configured, logged, and run locally — so that Phase 2 can spend its full attention on one service, Organization, without re-litigating infrastructure decisions.
Two structural decisions matter more than any framework choice: how many Go modules this repository will contain, and how much ceremony each service's internal package layout should carry. We answer both before writing main.go.
| Area | Course baseline | Introduced when Meridian needs it |
|---|---|---|
| Runtime and HTTP | Go 1.26+ and Gin | Workspace foundation, then Organization Service bootstrap |
| Data | PostgreSQL, pgx/v5, golang-migrate | The first owned schema and repository |
| Identity | JWT and bcrypt | Authentication Service |
| Configuration and logs | godotenv, cleanenv, zerolog | Foundation composition root |
| Validation and docs | go-playground/validator and Swaggo Swagger | Organization REST contract |
| Testing and quality | testify, Testcontainers, golangci-lint, gofumpt | The first automated test and CI loop |
| Distributed data | go-redis/v9 and segmentio/kafka-go | Rate limiting, caching, and asynchronous workflows |
This is a fixed implementation baseline, not the course structure. No phase exists merely to tour a library: each dependency appears only when a service has a concrete problem it solves, and later services reuse that decision instead of reinstalling or reteaching it.
The Organization service will be the only code in the entire platform allowed to write to the organizations table. Every other service that needs an organization's name or status asks Organization for it — directly today, and later through events — rather than reading its database. This rule is decided now so no future lesson has to argue about it.
Layered Architecture, Not Ports-and-Adapters
The Go community's default advanced answer to 'how do I structure a service' is often hexagonal architecture: domain, application, ports, adapters. It is a good answer for a service with several outbound integrations and complex domain invariants. Organization is neither of those things yet — it is a CRUD-shaped resource with a small state machine (active, suspended, archived) and one dependency, PostgreSQL. Four-person teams shipping CRUD-heavy admin platforms pay a real productivity tax for hexagonal ceremony they do not need in year one.
Instead, every service in this platform uses a flat, layered package structure: model, dto, repository, service, handler. The only concession this course makes toward testability is that the repository is always consumed through an interface, so PostgreSQL can be swapped for a fake in unit tests and for a real, ephemeral container in integration tests. That single seam buys almost all of hexagonal architecture's testing benefit without its indirection cost.
| Concern | Hexagonal (ports/adapters) | Layered (this course) |
|---|---|---|
| Packages per service | domain, application, port, adapter — 4+ | model, dto, repository, service, handler — 5, all flat |
| Swappable dependencies | Any adapter, by design | Only the repository, deliberately — that is the one dependency we actually swap |
| Cost to a new contributor | Must learn the ports vocabulary before touching code | Recognizable from any typical Go CRUD service in ten minutes |
| Right fit for | Services with several outbound integrations or rich domain logic | CRUD-shaped resources with a small, explicit state machine |
Why One Monorepo, Not One Repository per Service
Every HOA service will share three concerns: how it reads configuration, how it logs, and how it represents a business error. Splitting those into a polyrepo today would mean either duplicating three small packages across ten repositories, or standing up a private module proxy and a release process for internal libraries before a single feature ships. Neither is worth it at this team size. A monorepo with independent Go modules gets the sharing without the release ceremony: every module is still separately versioned and separately buildable, but they live at predictable relative paths and are composed locally with go.work — the subject of the next lesson.
Recording the Decision as an ADR, Not Just a Conversation
A decision that only exists in someone's memory gets re-argued the day that person is on leave. We record it as an Architecture Decision Record (ADR): a short, dated, append-only markdown file that states the context, the decision, and the consequences the team accepted. The first ADR in this repository documents exactly the two decisions above — layered services and a monorepo — so a contributor joining in Phase 3 can read one file instead of asking in chat.
# 0001. Monorepo with layered services
## Status
Accepted
## Context
The HOA platform will grow into several deployable services (Organization first,
then Resident, Payment, Complaint, and more). Each service needs configuration
loading, structured logging, and a shared error vocabulary. We must decide the
repository shape and the internal package shape before any code exists.
## Decision
- One git repository ("hoa-platform") holds every service and shared package.
- There is no root go.mod. Every module (each service, each pkg/* library) owns
its own go.mod and is composed locally through a single go.work file.
- Every service uses a flat layered package structure — model, dto, repository,
service, handler — instead of hexagonal ports-and-adapters. The repository is
always consumed through an interface so it can be faked in unit tests and
replaced by a real, ephemeral container in integration tests.
- Each service owns exactly one database and is the only writer of its own
tables. Other services never read another service's database directly.
## Consequences
- Shared code (env parsing, logging, semantic errors) lives once under pkg/ and
is imported by services/, never the reverse.
- Adding a service means adding a new services/<name> module and one line in
go.work — not restructuring an existing module.
- The team accepts that some future service may eventually need genuine
hexagonal boundaries (multiple outbound integrations, complex domain rules).
When that happens, it is scoped to that one service, not retrofitted
everywhere.
Applied exercise
Draft an ADR for a service this course has not built yet
Product wants a future 'Visitor' service that lets residents pre-approve guests and lets gate staff check them in. You have not written any code for it — only the ADR.
- Name the one fact the Visitor service must own exclusively (something no other service, including Organization, may write directly).
- State whether Visitor needs its own database or can share Organization's — and justify the answer using the ownership rule from this lesson.
- List two responsibilities that must stay in a client (a guard's mobile app), not in the Visitor service.
- Write the ADR using the same four headings as 0001: Status, Context, Decision, Consequences.
Deliverable
A new docs/adr/0002-visitor-service-boundary.md following the exact heading structure of 0001.
Completion checks
- The 'owns exclusively' fact is a durable business fact, not a UI concern.
- The database decision explicitly cites the one-writer-per-table rule.
- No heading from 0001 is renamed or dropped.
Why does this course choose a layered package structure over hexagonal ports-and-adapters for the Organization service?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.