Stage 7 · Master
Retrospective & What's Next
What Changed From the Plan
The original architecture was neat on paper; the shipped one is better because it admitted where the paper version was lying.
The Architecture We Thought We Were Building
The first draft of Fieldwork looked cleaner than the version we actually kept. Service boundaries were sharper, the gateway was supposed to be a relatively thin edge router, and Kafka showed up early in the drawings as if asynchronous workflows would naturally emerge wherever two services touched the same entity. That was a good teaching starting point because it surfaced the questions. It was not a good shipping plan unless we were willing to revise it.
What changed is not that the early modules were wrong. They were incomplete. Once real request paths, debugging needs, and operational constraints appeared, several 'obvious' architectural moves stopped earning their keep. The retrospective matters because this is exactly what happens in production engineering: the plan is allowed to lose to evidence.
| Area | Early expectation | What actually shipped |
|---|---|---|
| Gateway | Mostly routing plus auth verification | Routing, auth, coarse RBAC, request shaping, and trace propagation |
| Data ownership | Service-local persistence boundaries everywhere | A shared tenant-scoped Postgres schema stayed central longer than planned |
| Eventing | Kafka introduced early as a default coordination path | Used selectively once real side effects justified async complexity |
| Observability | Something we would add as services stabilized | Something we needed earlier because debugging distributed flows without it was too expensive |
The Gateway Became Thicker Than the First Draft
We originally wanted the gateway to stay thin: authenticate the token, route the request, get out of the way. In practice, once we added multi-tenancy, role claims, request IDs, rate limiting, and consistent public error handling, the gateway became an actual policy layer. We kept that change because duplicating those cross-cutting concerns inside every service would have been noisier and less reliable than accepting a slightly fatter edge.
The rejected alternative was not wrong in theory. It was wrong for this system once auth, tenancy, logging, rate limiting, and consistent edge behavior all needed to agree. At that point, refusing to centralize some policy would have been dogma, not cleanliness.
The Data Model Stayed More Centralized Than Planned
The second major divergence was persistence. Early architecture sketches made it tempting to imagine each service owning a sharply isolated store from the beginning. Fieldwork did not do that. It kept a shared Postgres schema keyed by tenant_id across users, memberships, workspaces, projects, and tasks because the operational cost of splitting storage earlier would have exceeded the value. Cross-entity queries and authorization checks were still too tightly coupled.
That decision traded some long-term autonomy for short-term coherence. We accepted it because introducing fake independence too early usually produces one of two bad outcomes: chatty synchronous calls for every query, or copied data before the team has earned the complexity of keeping it correct.
| Choice | Concrete benefit | Concrete cost |
|---|---|---|
| Shared tenant-scoped Postgres schema | Straightforward joins for membership and project authorization, simpler migrations early on | Weaker service-level persistence autonomy and a larger shared blast radius |
| Strict database-per-service from day one | Cleaner ownership story on diagrams | High coordination cost for reads and policy checks before async projections mature |
Async Design Arrived Later and More Selectively
Kafka was always in the course plan, but the role it ended up playing got narrower and more practical. Instead of turning every cross-service interaction into an event immediately, we used the outbox and Kafka where the system actually benefited from temporal decoupling: notifications, activity streams, derived projections, and non-critical downstream reactions. We rejected the broader 'events everywhere' instinct because it would have moved too much basic request comprehension out of traceable synchronous flows before observability and idempotency were mature enough.
One of the biggest plan changes was timing, not structure. We thought logs plus a few metrics would carry us longer. They did not. Distributed request tracing and service-level dashboards became necessary earlier because once the gateway, Redis, Postgres, and Kafka were all in the path, intuition stopped being a debugging strategy.
What the Divergence Actually Means
The most useful retrospective conclusion is not 'plans are useless'. It is more specific: early plans should be concrete enough to drive the next module and humble enough to be revised by the next real bottleneck. Fieldwork changed where the evidence was stronger than the diagram. That is not a betrayal of the design. It is the design maturing under contact with actual work.
- Expect edge layers to absorb more shared policy than your first whiteboard sketch admits.
- Do not force storage separation before the system has a credible strategy for cross-entity reads and authorization.
- Introduce async flows where delay tolerance and side-effect decoupling are real, not where architecture purity wants them.
- Pull observability forward in the build order once more than two infrastructure components participate in one request path.
What matters is whether each change made Fieldwork easier to operate, reason about, and extend. In this case, the answer was yes: the gateway became a policy boundary, the database stayed shared longer, async work became selective, and the course got more honest because of it.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.