Frontend architecture is the set of decisions that keeps user experience, state, data and teams understandable as an application grows.
Before we begin: let us learn this together
These are my frontend architecture self-notes, written as a mentoring conversation rather than a folder-pattern catalogue. You and I will begin with the pressure a product feels, ask who owns each rule and piece of state, and then choose boundaries that make change safer. Architecture is not impressive terminology. It is the set of decisions that lets a future developer understand where a change belongs and what it may affect.
1. “What should I understand about architecture starts with change?”
Good architecture makes likely changes local and important rules visible. It is not the number of folders or patterns.
src/
app/ composition, routes, providers
features/
courses/
lessons/
identity/
shared/
ui/
lib/
server/
Organise primarily by domain feature. Promote code to shared only after reuse and a stable meaning emerge.
2. “Can we unpack dependency direction?”
app → features → shared
UI → application use cases → domain contracts
infrastructure implements contracts
Domain rules should not import React components or a database client.
export interface CourseRepository {
find(id: string): Promise<Course | null>;
save(course: Course): Promise<void>;
}
export function makePublishCourse(repository: CourseRepository, clock: () => Date) {
return async (id: string) => {
const course = await repository.find(id);
if (!course) throw new NotFoundError();
const published = course.publish(clock());
await repository.save(published);
return published;
};
}
Add abstractions where they protect a meaningful boundary, not around every library call.
3. “Why does it matter to understand state ownership?”
Place state in the smallest correct owner:
- local UI: input, dialog, selection;
- URL: filters, page, shareable view;
- server/cache: remote records;
- session: identity and permissions;
- database: durable truth.
const [params, setParams] = useSearchParams();
const status = params.get("status") ?? "all";
Do not copy loader/query data into a global store without a concrete reason. Duplicated sources of truth create synchronization bugs.
4. “How should I think about model transitions?”
type EditorState =
| { kind: "closed" }
| { kind: "editing"; draft: string }
| { kind: "saving"; draft: string }
| { kind: "failed"; draft: string; message: string };
Reducers or state machines help when legal transitions matter. Several independent booleans often allow impossible combinations.
5. “Can you explain server state and caching?”
Remote data has freshness, deduplication, retries, invalidation and ownership concerns. Use router loaders, framework server components or a query library rather than recreating a cache casually.
const query = useQuery({
queryKey: ["course", courseId],
queryFn: ({ signal }) => api.getCourse(courseId, signal),
});
After mutation, update or invalidate the precise resource. Optimistic UI needs rollback and reconciliation.
6. “What should I understand about component boundaries?”
Separate policy and orchestration from reusable presentation where doing so improves clarity.
function CourseRoute() {
const course = useLoaderData() as Course;
const canEdit = usePermission("course.edit", course.id);
return <CourseScreen course={course} actions={canEdit ? <EditCourse /> : null} />;
}
Avoid giant components and premature universal components. Composition is often clearer than dozens of boolean props.
7. “Can we unpack design-system architecture?”
Build tokens, semantic primitives and documented interaction patterns.
<Dialog>
<Dialog.Title>Delete course?</Dialog.Title>
<Dialog.Body>This cannot be undone.</Dialog.Body>
<Dialog.Actions><Button variant="danger">Delete</Button></Dialog.Actions>
</Dialog>
The design system owns behavior and accessibility of the dialog; the feature owns the deletion policy.
8. “Why does it matter to understand error and loading boundaries?”
Place recovery where users can continue. A chart failure should not blank the entire dashboard. Route boundaries handle navigation/data errors; widget boundaries protect independent regions. Skeletons should resemble stable layout and avoid misleading controls.
9. “How should I think about aPI boundary and anti-corruption?”
Do not spread backend response shapes throughout UI code.
function toCourse(dto: CourseDto): Course {
return {
id: dto.course_id,
title: dto.display_name,
publishedAt: dto.published_at ? new Date(dto.published_at) : null,
};
}
Validate runtime data and translate it at the boundary. This contains API evolution and creates frontend-oriented domain language.
10. “Can you explain cross-cutting concerns?”
Authentication, authorization, analytics, feature flags, logging and internationalisation should have explicit entry points. Avoid hidden global helpers. Feature flags need owners and removal dates; analytics must respect privacy and stable event contracts.
11. “What should I understand about performance architecture?”
Route-split heavy areas, colocate state, avoid network waterfalls and keep client boundaries narrow. Measure before memoising. Performance often follows good ownership because fewer consumers rerender and less data travels.
12. “Can we unpack testing and governance?”
Test domain rules, feature integrations, API contracts and critical journeys. Use lint boundaries to prevent forbidden imports only when dependency rules are intentional. Record major decisions with short ADRs.
13. “Why does it matter to understand migration strategy?”
Do not rewrite a working application solely for architectural purity. Identify a painful seam, create a better boundary, move one vertical feature, measure the outcome and repeat. Strangler-style migration preserves delivery.
14. “How should I think about practice architecture?”
Design StudyDesk with course, lesson, identity and progress features. Draw state ownership, dependency direction, routes, API adapters and error boundaries. Add a new “quiz” feature and assess how many existing folders it must touch.
Mastery statement: scalable frontend architecture gives each state and rule one clear owner, directs dependencies toward stable concepts and creates boundaries aligned with user and business change.
15. “Can you explain architecture as a city map?”
A small village can grow around one road. A city needs districts, public routes, utility boundaries and building rules. Frontend architecture is similar: it organizes change so one new feature does not require edits everywhere.
Architecture is not a fashionable folder tree. It is the set of decisions that determine ownership, dependencies, communication and change cost.
16. “What should I understand about begin with quality attributes?”
Before selecting libraries, write the qualities the system needs:
- Can a learner complete a lesson on a slow phone?
- Can several teams release independently?
- Must the app work offline?
- Are permissions and auditability critical?
- How quickly must a new developer locate a feature?
17. “Can we unpack organize around change?”
A feature-oriented structure keeps collaborating code near its reason for changing:
src/
app/ # composition, router, providers
features/
courses/
api/
components/
model/
routes/
tests/
progress/
identity/
shared/
ui/ # truly reusable primitives
platform/ # HTTP, telemetry, storage adapters
Avoid a giant components/, hooks/, services/ arrangement where one feature is scattered across the repository. Also avoid putting everything in shared on its second use. Shared code has more consumers and therefore costs more to change.
18. “Why does it matter to understand dependency direction?”
Stable business concepts should not depend on volatile framework details.
// Feature model: framework-independent
export type CourseRepository = {
publish(courseId: string): Promise<Course>;
};
export function publishCourse(repository: CourseRepository, courseId: string) {
return repository.publish(courseId);
}
The HTTP adapter implements the interface; the use case does not import fetch, React or route globals. This is useful when rules are substantial. Do not wrap every trivial platform call solely to imitate an architecture diagram.
19. “How should I think about public feature APIs?”
Give each feature a small entry point:
// features/courses/index.ts
export { CourseCard } from "./components/CourseCard";
export { courseRoutes } from "./routes";
export type { CourseSummary } from "./model/course";
Other features import this public API instead of reaching into internal paths. Boundary lint rules can enforce the policy once the team agrees on it.
Circular imports usually signal unclear ownership. Move a genuinely shared concept downward or communicate through an explicit event/contract rather than letting features know each other's internals.
20. “Can you explain classify state before choosing a store?”
State has different owners:
| Kind | Example | Natural owner |
|---|---|---|
| URL state | course ID, search, page | router/URL |
| Server state | fetched course, cache freshness | query/data layer |
| Form state | draft title, field errors | form/route |
| Local UI state | open menu, selected tab | component |
| Auth/session | current identity | application boundary |
| Durable preference | theme | storage-backed preference service |
21. “What should I understand about derive instead of synchronize?”
This stores the same truth twice:
const [courses, setCourses] = useState<Course[]>([]);
const [publishedCount, setPublishedCount] = useState(0);
Prefer derivation:
const publishedCount = courses.filter(course => course.published).length;
Synchronized copies create invalid combinations. Memoize only if measurement shows the calculation is costly; correctness comes first.
22. “Can we unpack reducers and state machines?”
When many booleans describe one workflow, impossible states appear:
// Could all three become true?
{ isSaving: true, isSaved: true, hasError: true }
Use a discriminated union:
type SaveState =
| { status: "editing" }
| { status: "saving" }
| { status: "saved"; savedAt: Date }
| { status: "failed"; message: string };
A reducer centralizes allowed transitions. A formal state-machine library becomes valuable for parallel states, guards, delayed transitions or workflows shared across teams. Use the simplest representation that excludes invalid states.
23. “Why does it matter to understand server state is not ordinary global state?”
Fetched data has freshness, deduplication, retries, invalidation and ownership on the server. A query library or router data API can model these concerns.
function useCourse(id: string) {
return useQuery({
queryKey: ["course", id],
queryFn: () => coursesApi.get(id),
staleTime: 60_000,
});
}
After an update, invalidate or update the precise cache entry. Do not mirror query results into another store by default; two caches create disagreement.
24. “How should I think about uRL as durable interface state?”
If a user should bookmark, share, refresh or use Back/Forward for a state, consider the URL.
const params = new URLSearchParams(location.search);
const page = Math.max(1, Number(params.get("page") ?? 1));
const query = params.get("q") ?? "";
Validate URL values just like other input. Preserve meaningful parameters when navigating. Avoid storing transient secrets or enormous state in the URL.
25. “Can you explain component responsibilities?”
A component is easier to understand when it has one clear reason to change. Separate data orchestration from reusable presentation where that distinction helps:
function CourseRoute() {
const { course, permissions } = useLoaderData<typeof loader>();
return <CoursePage course={course} canEdit={permissions.canEdit} />;
}
function CoursePage({ course, canEdit }: Props) {
return <main><CourseHeader course={course} actions={canEdit && <EditButton />} /></main>;
}
Composition slots often age better than a universal component with flags such as compact, admin, editable, modal, dashboard and special.
26. “What should I understand about design system versus product feature?”
The design system owns reusable appearance and interaction mechanics: buttons, focus, dialogs, spacing and tokens. A feature owns business meaning: whether a course may be deleted and what confirmation copy is required.
Design tokens should express semantic decisions:
:root {
--color-surface-raised: #ffffff;
--color-text-danger: #9f1239;
--space-control-inline: .875rem;
}
Avoid feature-specific concepts leaking into primitive components. Button variant="danger" is reusable; Button deletingCourse is not.
27. “Can we unpack aPI adapters and runtime validation?”
TypeScript types disappear at runtime, so an external response needs validation:
const CourseDto = z.object({
course_id: z.string(),
display_name: z.string(),
published_at: z.string().datetime().nullable(),
});
async function getCourse(id: string): Promise<Course> {
const response = await http.get(`/courses/${id}`);
const dto = CourseDto.parse(response);
return {
id: dto.course_id,
title: dto.display_name,
publishedAt: dto.published_at ? new Date(dto.published_at) : null,
};
}
This anti-corruption layer stops transport naming and date strings spreading through components. It also provides one place to handle versions and deprecations.
28. “Why does it matter to understand authentication and authorization boundaries?”
The frontend may hide actions for clarity, but the server must enforce permissions. Model capabilities rather than repeating raw role checks everywhere:
type CourseCapabilities = {
canEdit: boolean;
canPublish: boolean;
canDelete: boolean;
};
Derive these at a trusted boundary or receive them from an authorized API representation. A route guard improves navigation experience; it is not protection against direct API calls.
29. “How should I think about error, loading, empty and stale states?”
Every data boundary should intentionally define:
- initial loading;
- useful stale data during refresh;
- empty success;
- validation failure;
- permission failure;
- recoverable dependency failure;
- unexpected failure.
30. “Can you explain routing as architecture?”
Routes define feature entry points, data dependencies and code-splitting boundaries. Load essential route data before rendering when supported to avoid request waterfalls.
const routes = [{
path: "courses/:courseId",
lazy: () => import("@features/courses/routes/course-route"),
loader: courseLoader,
errorElement: <CourseRouteError />,
}];
Deep links must work after a hard refresh. Decide which layouts persist and where authentication, titles and analytics belong.
31. “What should I understand about form architecture?”
Keep field mechanics, validation schema and submission policy distinct. The browser owns useful native semantics; the feature owns business validation; the API remains authoritative.
const CreateCourse = z.object({
title: z.string().trim().min(1).max(120),
visibility: z.enum(["private", "public"]),
});
Support pending, field errors, form errors, duplicate submission and unsaved navigation. For optimistic updates, define rollback and conflict behaviour before adding animation.
32. “Can we unpack internationalization is structural?”
Text length, grammar, plural rules, date/number formatting, writing direction and locale routing affect component APIs.
const formatter = new Intl.NumberFormat(locale, {
style: "percent",
maximumFractionDigits: 0,
});
Do not concatenate translated fragments such as "Hello " + name; translators need the whole message and placeholders. Test long translations and RTL before layouts harden.
33. “Why does it matter to understand performance through boundaries?”
Architecture influences performance before micro-optimizations:
- route-split code users do not need yet;
- avoid sequential data requests when dependencies are known;
- keep state near consumers to reduce update fan-out;
- virtualize genuinely large lists;
- send appropriately sized images;
- avoid hydrating static regions unnecessarily;
- measure bundles and user-centric performance.
memo and useMemo everywhere.
34. “How should I think about testing by architectural layer?”
Match tests to responsibilities:
- domain rules: pure unit tests;
- component interaction: role/name-based tests;
- API adapters: contract fixtures and malformed responses;
- feature slices: integration tests;
- route journeys: end-to-end tests;
- dependency rules: static checks.
35. “Can you explain monorepos and multiple applications?”
A monorepo can simplify atomic changes, shared tooling and discovery. It also needs ownership, dependency rules and efficient builds. Package boundaries should represent real versioning or ownership needs, not every folder.
apps/
learner-web/
admin-web/
packages/
design-system/
course-contracts/
test-helpers/
Keep package public APIs deliberate. Independent packages do not justify a distributed architecture by themselves.
36. “What should I understand about microfrontends: a team-scaling tradeoff?”
Microfrontends may help large autonomous teams deploy separate business areas. Costs include duplicated runtime, inconsistent user experience, cross-app communication, routing, observability and integration testing.
Do not choose them to fix untidy folders in one team. First establish modular feature boundaries inside one deployable application. Split deployment only when organizational independence justifies operational complexity.
37. “Can we unpack architecture Decision Records?”
An ADR is a short memory of a consequential decision:
# Use route loaders for essential course data
Status: accepted
Context: effects caused loading waterfalls and inconsistent errors.
Decision: essential route data loads at the route boundary.
Consequences: routes own cancellation and errors; widgets may still fetch optional data.
Record context, decision, alternatives and consequences. An ADR is not permanent law; replace it with a newer decision when conditions change.
38. “Why does it matter to understand evolution without a rewrite?”
Choose one painful seam—perhaps API response mapping scattered across screens. Add a boundary for new work, move one vertical feature, measure defects and delivery time, then continue.
Use adapters so old and new structures coexist temporarily. Delete compatibility code when migration finishes. A permanent “temporary” layer is architecture debt, so give it an owner and exit condition.
39. “How should I think about studyDesk architecture workshop?”
Design the “complete lesson” journey:
- Route loads course and current progress.
- API adapter validates transport data.
- Feature model exposes learner-friendly types.
- Button submits an idempotent completion command.
- Server-state cache updates optimistically or after confirmation.
- Error handling restores prior state and explains retry.
- Analytics records a stable event without private note text.
40. “Can you explain architecture review questions?”
For a new feature, ask:
- Which business capability owns it?
- Where is each kind of state authoritative?
- What is the feature's public API?
- Which direction do dependencies flow?
- How are external data and errors translated?
- Can loading, empty, failure and permission states be recovered locally?
- What will be measured and tested?
- What temporary decision needs a removal date?
41. “What should I understand about seven-day revision plan?”
- Day 1: map features, quality attributes and current pain points.
- Day 2: classify every important state by owner and lifetime.
- Day 3: define feature public APIs and dependency direction.
- Day 4: build an API adapter with runtime validation.
- Day 5: design route, form, loading and error boundaries.
- Day 6: add tests and measure a real performance journey.
- Day 7: write one ADR and migrate one vertical slice safely.
42. “Faz, how do I know whether my frontend needs architecture?”
The moment a codebase must support change, it has architecture. The question is whether those decisions are visible and helpful.
A small experiment can live comfortably in a few files. When several features, developers and release cycles arrive, hidden ownership becomes expensive. A new quiz feature touches authentication internals, generic components, global state and unrelated routes; nobody knows which changes are safe.
Do not respond by importing an enterprise folder template. Begin with the pressures: team size, release independence, offline needs, performance budgets, domain complexity and expected change. Architecture should remove a demonstrated kind of confusion.
43. “What does ‘scalable’ actually mean here?”
Scale has dimensions. User scale stresses network, rendering and backend capacity. Feature scale stresses boundaries and discoverability. Team scale stresses ownership and coordination. Time scale stresses upgradeability and institutional memory.
Write quality-attribute scenarios:
A new developer can locate the course-publishing rule within ten minutes.
The quiz team can release without editing identity internals.
A learner on a slow phone reaches lesson text within the agreed budget.
An API field rename is contained inside one adapter.
Now a proposed boundary can be judged against real needs. “We use clean architecture” is not a measurable outcome.
44. “Should I organize files by technical type or product feature?”
When features change together, keep their route, UI, model, API adapter and tests close.
features/
courses/
api/
components/
model/
routes/
tests/
progress/
identity/
A global components/, hooks/, services/, types/ structure makes one feature a scavenger hunt. A feature structure makes the business capability visible.
Some code genuinely spans features: design-system primitives, HTTP infrastructure and cross-cutting telemetry. Give that shared code a narrow public contract. Shared code has more consumers, so promote reluctantly rather than on the second use.
45. “What should a feature be allowed to import?”
Define dependency direction. A feature may import shared primitives and platform adapters. Another feature should normally import only its public API, not reach into internal files.
// features/courses/index.ts
export { CourseCard } from "./components/CourseCard";
export { courseRoutes } from "./routes";
export type { CourseSummary } from "./model/CourseSummary";
This creates a reviewable boundary. If progress needs course identity, perhaps a small shared domain identifier belongs below both. If it needs to react to course publication, an event or orchestration layer may be clearer than importing course repository internals.
Boundary linting helps only after the team agrees on the rule. A tool enforcing an arbitrary diagram creates ceremony, not architecture.
46. “How do ports and adapters help a frontend?”
A port describes what the feature needs without naming transport. An adapter fulfills it with HTTP, IndexedDB or a test implementation.
export interface CourseRepository {
get(id: CourseId, signal?: AbortSignal): Promise<Course>;
save(course: Course, version: string): Promise<SavedCourse>;
}
export class HttpCourseRepository implements CourseRepository {
constructor(private readonly http: HttpClient) {}
get(id: CourseId, signal?: AbortSignal) {
return this.http.get(`/courses/${id}`, { signal }).then(toCourse);
}
}
This is valuable when domain logic deserves independence from transport or multiple adapters exist. Do not wrap every fetch in five interfaces. Abstraction earns its cost by stabilizing a meaningful boundary.
47. “Where should each kind of state live?”
Name the state before choosing a store.
- Shareable/filterable navigation state belongs in the URL.
- Fetched remote data belongs in a server-state/query boundary.
- Unsaved field values belong near the form.
- An open tooltip belongs in the component.
- Current identity belongs at the application/session boundary.
- A durable preference belongs in a storage-backed preference service.
Do not store derived values separately:
const completedCount = lessons.filter(lesson => lesson.completed).length;
If you store both lessons and count, every mutation must keep them synchronized and invalid combinations become possible.
48. “How is server state different from ordinary client state?”
Server state is a local observation of remote authority. It has freshness, loading, deduplication, invalidation, retry and conflict concerns.
useQuery({
queryKey: ["course", courseId],
queryFn: ({ signal }) => coursesApi.get(courseId, { signal }),
staleTime: 60_000,
});
Do not copy query data into a global store by default. Two caches create disagreement. Transform for display through selectors and update/invalidate the authoritative query entry after mutation.
Freshness is a product decision. A public course title can tolerate a minute; a payment status may require immediate confirmation.
49. “When is optimistic UI honest?”
Optimism is a temporary prediction that the server will accept an operation. Use it when success is likely, rollback is understandable and duplicate effects are controlled.
const previous = queryClient.getQueryData(key);
queryClient.setQueryData(key, optimisticCourse);
try {
const saved = await api.updateCourse(command);
queryClient.setQueryData(key, saved);
} catch (error) {
queryClient.setQueryData(key, previous);
showRecoverableError(error);
}
For money movement, irreversible publication or conflicts, a visible pending state may be more honest. Design rollback, reconciliation and accessibility announcements before adding animation.
Server idempotency and version conditions still protect reality. A smooth client cannot guarantee acceptance.
50. “When do reducers or state machines become useful?”
When several booleans can describe impossible combinations, model one explicit state.
type PublishState =
| { status: "editing" }
| { status: "validating" }
| { status: "publishing" }
| { status: "published"; at: Date }
| { status: "failed"; problem: PublishProblem };
A reducer centralizes transitions. A state-machine library adds guards, parallel regions, delayed transitions and visualization when the workflow warrants them.
Do not use a machine because the diagram looks sophisticated. Use it when making allowed states and transitions explicit removes real defects.
51. “What belongs in the design system and what stays in a feature?”
The design system owns reusable interaction mechanics and visual language: button behavior, focus, dialog mechanics, spacing, typography and tokens. The course feature owns whether a course may be deleted, the confirmation wording and the command that performs deletion.
<Dialog open={open} onClose={cancel}>
<Dialog.Title>Delete “{course.title}”?</Dialog.Title>
<Dialog.Body>This cannot be undone.</Dialog.Body>
<Dialog.Actions>
<Button onClick={cancel}>Cancel</Button>
<Button variant="danger" onClick={confirm}>Delete course</Button>
</Dialog.Actions>
</Dialog>
The primitive guarantees focus containment/restoration and accessible naming. The feature supplies policy and content. Avoid putting courseId into a universal Dialog API.
52. “Why translate API data at one boundary?”
External data uses transport names, strings and versions. The frontend needs stable domain language.
const CourseDto = z.object({
course_id: z.string(),
display_name: z.string(),
published_at: z.string().datetime().nullable(),
});
function toCourse(value: unknown): Course {
const dto = CourseDto.parse(value);
return {
id: CourseId(dto.course_id),
title: dto.display_name,
publishedAt: dto.published_at ? new Date(dto.published_at) : null,
};
}
TypeScript disappears at runtime. Validation protects the boundary; mapping contains change. If backend naming changes, components do not all become migration sites.
53. “Where should loading and error boundaries sit?”
Place recovery where the user can continue. A recommendations widget failure should not remove the lesson. A session-loader failure may affect the route or application.
For every data region, design:
- initial loading;
- stale data during refresh;
- empty success;
- validation or permission failure;
- recoverable dependency failure;
- unexpected failure.
54. “How should routes influence architecture?”
Routes are feature entry points and natural boundaries for data, authorization experience, errors and code splitting.
{
path: "courses/:courseId",
lazy: () => import("@features/courses/routes/course"),
loader: courseLoader,
errorElement: <CourseRouteError />,
}
Load essential route data without sequential effect waterfalls. Let the URL own bookmarkable state. Make deep links survive hard refresh. Define which layouts persist and where titles, analytics and focus restoration occur.
Route guards improve experience but do not secure APIs. The server remains authoritative.
55. “How do event-driven workflows reduce or create coupling?”
An event states that something happened without commanding every consumer directly.
events.publish({
type: "course.published",
courseId,
occurredAt: clock.now(),
});
Analytics, notifications and cache invalidation may react. This reduces direct imports, but hidden event flows can become harder to trace. Keep event names/schema stable, document owners and avoid using a global event bus for ordinary parent-child communication.
Frontend events are usually in-process and not durable. Do not treat them as guaranteed business delivery. Critical server workflows need durable backend semantics.
56. “When is a monorepo useful?”
A monorepo can simplify atomic changes, shared tooling and discovery across applications/packages. It also needs ownership, dependency rules, caching and build performance.
Create a package when it has a real public API or independent consumer—not for every folder. Shared packages should not import application internals. Versioning may be synchronized or independent according to release needs.
Repository topology does not create architecture by itself. A monorepo containing tangled imports is still tangled; several repositories connected by undocumented runtime contracts can be worse.
57. “Do microfrontends solve frontend architecture?”
They can support organizational independence when autonomous teams own distinct business areas and require separate deployment. They add runtime duplication, cross-app navigation, consistency, communication, observability and testing costs.
Do not split deployment to repair an untidy folder structure. First create modular feature boundaries inside one application. If team/release pressure still demands separation, choose integration method, shared dependencies, failure isolation and design-system governance explicitly.
A distributed frontend mirrors organizational boundaries. If those boundaries are unstable, the technical seams will be unstable too.
58. “How do we migrate architecture without a rewrite?”
Pick one painful seam and one vertical feature. Introduce the desired boundary for new work, adapt old code at the edge, move behavior gradually and measure whether defects or change cost improve.
old API shapes → adapter → new Course model → new feature screen
Do not create a second system with no exit plan. Give compatibility layers an owner and removal condition. Delete migrated paths as confidence grows.
Rewrites pause delivery, discard working edge-case knowledge and often reproduce old problems under new names. Incremental migration lets production evidence guide architecture.
59. “What should an architecture decision record contain?”
Keep it short:
# Load essential course data at route boundaries
Status: accepted
Context: component effects caused waterfalls and inconsistent errors.
Decision: route loaders own essential course data and cancellation.
Consequences: routes gain responsibility; optional widgets may still query locally.
Alternatives: component effects; one global bootstrap request.
Record context, decision, alternatives and consequences. An ADR is memory, not permanent law. Supersede it when conditions change rather than silently contradicting it.
60. “How can reviews protect architecture without becoming bureaucracy?”
Ask practical questions:
- Which feature owns this behavior?
- Where is its state authoritative?
- What public contract crosses the boundary?
- Does dependency direction remain clear?
- How are external data and failures translated?
- Can the user recover locally?
- Which test proves the important rule?
- What temporary compromise needs an exit date?
61. “Can we design one StudyDesk feature together?”
Take “complete lesson.”
The route loads course and current progress. The API adapter validates transport data. The progress feature owns the completion command and optimistic state. The server-state cache owns remote freshness. The button is a design-system primitive but the feature supplies “Mark lesson complete.” The API enforces identity, enrollment and idempotency.
Now add offline behavior. The requirement changes architecture: commands need durable client storage, sync status, conflict policy and retry ownership. We did not add IndexedDB because it was fashionable; the product pressure demanded a new boundary.
Test the pure progress rule, the feature interaction, malformed API data, duplicate completion and one real journey. Record analytics without private note text. Place errors where the learner can retry without losing reading position.
62. “What should I remember after this architecture conversation?”
Junior: “Which architecture pattern should I choose?”
Faz: “First name the change or risk you need to manage. A pattern is useful only if its tradeoff serves that pressure.”
Junior: “Where should state go?”
Faz: “At the narrowest owner whose lifetime and authority match it. URL state in the URL, remote state in the query boundary, temporary interaction near the component.”
Junior: “How do I know the architecture is improving?”
Faz: “A feature has a clear home, dependencies point toward stable concepts, external change is contained, failures recover locally and developers can explain the design without mythology.”
That is enough. Architecture should make change calmer.
