From Requirements to Boundaries
From Requirements to Boundaries
Architecture starts with the write paths: list the entities that change (orders, users, files), the operations on them, and who calls them. Each cluster of behavior that changes for one reason becomes a module:
apps/web # UI, talks only to apps/api
apps/api # HTTP layer: routing, auth, validation
packages/shared # types + validation schemas shared by both
The HTTP layer stays thin: parse, authorize, delegate, serialize. Domain logic lives where it can be unit-tested without a socket. Rules worth writing down: apps/* may import packages/*, never the reverse, and no two packages import each other — a cycle means the boundary is wrong.
Folder Structure and Module Rules
Folder Structure and Module Rules
Feature-first structure keeps related files together and makes ownership obvious:
apps/api/src/
modules/orders/
routes.ts # transport only
service.ts # business rules
repo.ts # persistence
orders.test.ts
lib/ # cross-cutting: db, logger, config
Rules that keep it healthy: import across features only through service.ts or a shared package; keep lib/ free of feature knowledge; colocate tests with code. Declare the policy in tooling — ESLint no-restricted-imports or dependency-cruiser — because folder conventions without enforcement rot within a quarter.
Data Flow and Pitfalls
Data Flow and Pitfalls
Decide now: server state (fetched, cached, mutated) versus client UI state, and which layer owns each. A common plan: API returns plain JSON shaped like shared types; the client keeps server data in a query cache (React Query/SWR) and UI state in local stores; validation schemas in shared guard both ends.
Pitfalls. Premature microservices — network boundaries replace type checking with retries; start as one deployable with internal modules. Duplicated types between frontend and backend drift silently — generate or share them. Leaking database rows as API responses couples clients to schema.
Interview: draw the request path, name each package's one responsibility, and defend a boundary you would NOT split yet.
Quiz
1. Where should business rules live in a layered full-stack app?
2. What does "apps may import packages, never the reverse" prevent?
3. When are two packages importing each other a design problem?
Flashcards
Question
Dependency rule
Click to reveal answer
Answer
Inner packages know nothing of outer ones; apps → packages only, cycles forbidden.
Question
Feature-first structure
Click to reveal answer
Answer
routes/service/repo/test per feature under one folder; lib holds only cross-cutting code.
Question
Server vs client state
Click to reveal answer
Answer
Fetched data belongs in a query cache; ephemeral UI state in local stores.
Question
Premature microservice
Click to reveal answer
Answer
Splitting before boundaries are stable, trading type checks for network failure modes.
Revision Notes
Key Takeaways
- 1. Boundaries follow reasons to change, not file-count aesthetics
- 2. Enforce import rules in lint config, not tribal knowledge
- 3. Share types/schemas from one package; never hand-copy them
Interview Tips
- • Walk a request end to end and name the single responsibility of each folder
- • Present one boundary you deliberately kept merged and the trigger to split it later
Cheat Sheet
apps/* → packages/shared, no cycles; feature folder = routes/service/repo/test; API returns shared types; query cache for server state; enforce imports via ESLint.
Continue Learning
Before You Start
- No prerequisites listed.