|
1 | 1 | # Monorepo by Default: Monolith or Polylith |
2 | 2 |
|
3 | | -Frame assumes a **monorepo-first** layout that can run as: |
| 3 | +Frame uses a **monorepo-first** layout. You can run the same codebase as: |
4 | 4 |
|
5 | | -- **Monolith**: one binary, one service |
6 | | -- **Polylith**: multiple independent binaries from one repo |
| 5 | +- **Monolith**: one Frame service, one mux, many routes. |
| 6 | +- **Polylith**: many independent binaries, each service deployed separately. |
7 | 7 |
|
8 | | -Both modes share the same shared packages and conventions. The only difference is **how many entrypoints you build**. |
9 | | - |
10 | | -## Canonical Layout (Monorepo) |
| 8 | +## Canonical Layout |
11 | 9 |
|
12 | 10 | ```text |
13 | 11 | /README.md |
14 | 12 | /go.mod |
15 | 13 | /cmd |
16 | | - /monolith |
17 | | - main.go |
18 | | - /users |
19 | | - main.go |
20 | | - /billing |
21 | | - main.go |
| 14 | + /users/main.go |
| 15 | + /billing/main.go |
22 | 16 | /apps |
23 | 17 | /users |
24 | | - /cmd |
25 | | - /users |
26 | | - main.go |
27 | | - /users/Dockerfile |
28 | | - /service |
| 18 | + /cmd/main.go |
| 19 | + /service/routes.go |
| 20 | + /queues |
29 | 21 | /config |
30 | 22 | /migrations |
31 | 23 | /tests |
| 24 | + /Dockerfile |
32 | 25 | /billing |
33 | | - /cmd |
34 | | - /billing |
35 | | - main.go |
36 | | - /billing/Dockerfile |
37 | | - /service |
| 26 | + /cmd/main.go |
| 27 | + /service/routes.go |
| 28 | + /queues |
38 | 29 | /config |
39 | 30 | /migrations |
40 | 31 | /tests |
| 32 | + /Dockerfile |
41 | 33 | /pkg |
42 | | - /shared |
43 | 34 | /plugins |
44 | 35 | /openapi |
| 36 | + /shared |
45 | 37 | /configs |
46 | 38 | /Dockerfile |
47 | 39 | ``` |
48 | 40 |
|
49 | | -## Monolith Mode |
50 | | - |
51 | | -A single entrypoint composes multiple modules into one binary and runs one Frame service with one mux: |
| 41 | +## Monolith Mode (Single Service) |
52 | 42 |
|
53 | | -```text |
54 | | -/cmd/monolith/main.go |
55 | | -/apps/users/service |
56 | | -/apps/billing/service |
57 | | -``` |
| 43 | +Monolith means **one `frame.Service` + one `http.ServeMux`**. Multiple app routes are mounted into that one mux. |
58 | 44 |
|
59 | | -All routes are wired into the same mux in one process. This is ideal for: |
| 45 | +Example shape: |
60 | 46 |
|
61 | | -- fast local development |
62 | | -- smaller deployments |
63 | | -- shared runtime state |
64 | | - |
65 | | -## Polylith Mode (Composable) |
| 47 | +```go |
| 48 | +mux := http.NewServeMux() |
| 49 | +users.RegisterRoutes(mux) |
| 50 | +billing.RegisterRoutes(mux) |
66 | 51 |
|
67 | | -Each service has its **own entrypoint** under `/apps/<service>/cmd/<service>`, and a Dockerfile next to it. Shared libraries live in `/pkg`: |
| 52 | +ctx, svc := frame.NewService( |
| 53 | + frame.WithName("monolith"), |
| 54 | + frame.WithHTTPHandler(mux), |
| 55 | +) |
68 | 56 |
|
69 | | -```text |
70 | | -/apps/users/cmd/users/main.go |
71 | | -/apps/users/cmd/users/Dockerfile |
72 | | -/apps/billing/cmd/billing/main.go |
73 | | -/apps/billing/cmd/billing/Dockerfile |
74 | | -/pkg/... |
| 57 | +if err := svc.Run(ctx, ":8080"); err != nil { ... } |
75 | 58 | ``` |
76 | 59 |
|
77 | | -Each binary is independent, but uses the same `/apps` and `/pkg` packages. This matches the structure used in `service-profile`. |
78 | | - |
79 | | -## How to Switch Modes |
80 | | - |
81 | | -You do **not** need to restructure anything. You only: |
82 | | - |
83 | | -- add or remove `apps/<service>/cmd/<service>/main.go` entrypoints |
84 | | -- build different binaries or Docker images |
| 60 | +## Polylith Mode (Independent Binaries) |
85 | 61 |
|
86 | | -This makes the repo **composable** by default. |
| 62 | +Each app has its own binary entrypoint and Dockerfile: |
87 | 63 |
|
88 | | -## Recommended Conventions |
| 64 | +- `apps/<service>/cmd/main.go` |
| 65 | +- `apps/<service>/Dockerfile` |
89 | 66 |
|
90 | | -- `/apps/<service>` is the source of truth for a service |
91 | | -- `/apps/<service>/cmd/<service>` is the polylith entrypoint |
92 | | -- `/cmd/<service>` is the monorepo-level entrypoint (optional) |
93 | | -- `/pkg` is shared infrastructure and cross-cutting plugins |
94 | | -- `/configs` holds environment or YAML configs for all services |
| 67 | +The repo-level `cmd/<service>/main.go` gives a consistent top-level build/run entrypoint. |
95 | 68 |
|
96 | 69 | ## One-Command Scaffold |
97 | 70 |
|
98 | | -Frame includes a scaffold tool that creates the monorepo layout with per-service entrypoints and Dockerfiles. |
99 | | - |
100 | 71 | ```bash |
101 | 72 | go run github.com/pitabwire/frame/cmd/frame@latest init \ |
102 | 73 | -root . \ |
103 | 74 | -services users,billing \ |
104 | 75 | -module your/module |
105 | 76 | ``` |
106 | 77 |
|
107 | | -This generates: |
| 78 | +`-module` is optional. If omitted, Frame tries `go.mod`, then falls back to `example.com/project`. |
108 | 79 |
|
109 | | -- `/apps/<service>/cmd/<service>/main.go` |
110 | | -- `/apps/<service>/cmd/<service>/Dockerfile` |
111 | | -- `/cmd/monolith/main.go` |
112 | | -- `/Dockerfile` |
113 | | -- `/pkg` and `/configs` folders |
| 80 | +Generated artifacts: |
114 | 81 |
|
115 | | -## Example: Polylith Entry Point |
| 82 | +- `apps/<service>/cmd/main.go` |
| 83 | +- `apps/<service>/service/routes.go` |
| 84 | +- `apps/<service>/Dockerfile` |
| 85 | +- `cmd/<service>/main.go` |
| 86 | +- `Dockerfile` |
| 87 | +- `pkg` and `configs` |
116 | 88 |
|
117 | | -```go |
118 | | -package main |
| 89 | +## Build Patterns |
119 | 90 |
|
120 | | -import ( |
121 | | - "context" |
122 | | - "log" |
| 91 | +Polylith binary: |
123 | 92 |
|
124 | | - "github.com/pitabwire/frame" |
125 | | - "your/module/apps/users/service" |
126 | | -) |
| 93 | +```bash |
| 94 | +go build ./apps/users/cmd |
| 95 | +``` |
127 | 96 |
|
128 | | -func main() { |
129 | | - ctx, svc := frame.NewService( |
130 | | - frame.WithName("users"), |
131 | | - frame.WithHTTPHandler(service.Router()), |
132 | | - ) |
| 97 | +Monorepo-level binary for one app: |
133 | 98 |
|
134 | | - if err := svc.Run(ctx, ":8080"); err != nil { |
135 | | - log.Fatal(err) |
136 | | - } |
137 | | -} |
| 99 | +```bash |
| 100 | +go build ./cmd/users |
138 | 101 | ``` |
139 | 102 |
|
140 | | -## Example: Monolith Entry Point |
| 103 | +Single-binary monolith is generated from blueprints in monolith mode (`frame build`), producing `cmd/main.go` that composes all routes into one mux. |
141 | 104 |
|
142 | | -```go |
143 | | -package main |
| 105 | +## Docker Build Patterns |
144 | 106 |
|
145 | | -import ( |
146 | | - "context" |
147 | | - "log" |
148 | | - "net/http" |
| 107 | +Monorepo-level Dockerfile (`/Dockerfile`) uses a multi-stage builder and copies: |
149 | 108 |
|
150 | | - "github.com/pitabwire/frame" |
151 | | - "your/module/apps/users/service" |
152 | | - "your/module/apps/billing/service" |
153 | | -) |
| 109 | +- `/apps` |
| 110 | +- `/pkg` |
| 111 | +- `/cmd` |
| 112 | + |
| 113 | +Build an app via top-level cmd entrypoint: |
154 | 114 |
|
155 | | -func main() { |
156 | | - mux := http.NewServeMux() |
157 | | - service.RegisterRoutes(mux) |
158 | | - billing.RegisterRoutes(mux) |
| 115 | +```bash |
| 116 | +docker build -t users-service --build-arg APP=users . |
| 117 | +``` |
159 | 118 |
|
160 | | - ctx, svc := frame.NewService( |
161 | | - frame.WithName("monolith"), |
162 | | - frame.WithHTTPHandler(mux), |
163 | | - ) |
| 119 | +Per-service Dockerfile (`/apps/<service>/Dockerfile`) copies: |
164 | 120 |
|
165 | | - if err := svc.Run(ctx, ":8080"); err != nil { |
166 | | - log.Fatal(err) |
167 | | - } |
168 | | -} |
| 121 | +- `/apps/<service>` |
| 122 | +- `/pkg` |
| 123 | + |
| 124 | +Build a single polylith app: |
| 125 | + |
| 126 | +```bash |
| 127 | +docker build -t users-service -f apps/users/Dockerfile . |
169 | 128 | ``` |
0 commit comments