FullStack Course LogoFullStack Course
Module: Machine Coding
Machine Coding·237·10 MIN READ

237: Project Structure, Boundaries, and Incremental Architecture

TOPICS COVERED: Project Structure, Boundaries, and Incremental Architecture

Learning outcomes

By the end of this lesson, you should be able to:

  • explain and apply feature boundaries in a realistic implementation;
  • explain and apply layer restraint in a realistic implementation;
  • explain and apply API adapters in a realistic implementation;
  • explain and apply pure helpers in a realistic implementation;
  • explain and apply dependency direction in a realistic implementation.

Prerequisites and retrieval

This lesson builds on the 01–06 foundation and the lessons that came before it in this module. Before you start, retrieve one concrete example from an earlier project in which the same concern showed up. Perhaps a component became responsible for fetching and transforming data, or two features began importing each other's internals. The point is not to memorize architecture vocabulary. The point is to make a defensible design decision in a timed machine-coding exercise, while keeping the result readable, accessible, testable, and straightforward to extend under interview pressure.

Terminology

  • Feature boundaries: Group code around a cohesive feature or domain when that makes the code easier to navigate. The UI, hooks, tests, and small helpers for one capability should be discoverable together.
  • Layer restraint: Avoid controller, service, and repository abstractions in a frontend challenge unless they represent a real boundary. An extra layer has a cost, and the layer should earn that cost.
  • API adapters: Keep fetch and transport details in a small adapter that returns parsed or typed data. The rest of the application should not need to know how a URL is built or how a response is decoded.
  • Pure helpers: Move sorting, filtering, formatting, validation, and state transitions into pure functions when that makes them easier to test and removes noise from the component.
  • Dependency direction: Higher-level feature code should depend on stable helper or component contracts. It should not reach into deep implementation details of sibling features.
  • Incremental design: Begin with the simplest structure that supports the first vertical slice. Let a second use case demonstrate a genuine abstraction before you introduce a framework or a generalized architecture in advance.

Mental model

Treat Project Structure, Boundaries, and Incremental Architecture as a design problem with observable inputs, outputs, invariants, and failure modes. Even a timed codebase needs enough structure for its data flow to be obvious and for one feature to change without unrelated files becoming part of the change. A strong implementation makes assumptions visible, limits uncertainty at boundaries, and leaves evidence—tests, types, constraints, metrics, or diagrams—that explains why the design is safe.

A useful sequence for both an interview and production work is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump straight from a requirement to a library call. First state what must remain true. Then choose the mechanism that enforces those conditions. That habit is especially valuable when time is limited: it keeps the first implementation small without making its assumptions invisible.

Deep dive

1. Feature boundaries

Group code by cohesive feature or domain when possible. A screen, its hooks, tests, and small supporting helpers should be easy to find together when they all serve the same capability. This makes a change easier to scope and gives the feature a clearer ownership boundary.

Decision rule: Use feature boundaries deliberately when they make a contract or invariant easier to prove. If the grouping merely saves typing while hiding an important assumption, prefer the more explicit design.

2. Layer restraint

Do not add controller, service, or repository abstractions to a frontend challenge just because those names are familiar. In a machine-coding exercise, each layer adds indirection, naming, and more places for a reader to look. Use one when it isolates a real boundary—such as transport, persistence, or a separately meaningful domain rule—not when it gives a small component an impressive-looking shape.

Machine coding rewards clarity rather than ceremonial architecture.

Decision rule: Use layer restraint deliberately when it makes a contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

3. API adapters

Keep fetch and other transport details in a small adapter that returns parsed or typed data. Components should not each reconstruct URLs, check response status, and decode JSON. Centralizing that boundary gives the rest of the feature one contract and makes transport failures easier to test.

The adapter is not a reason to hide every decision. Its job is narrower: translate an external response into the shape the feature understands, while preserving meaningful failure behavior.

Decision rule: Use API adapters deliberately when they make a contract or invariant easier to prove. If they only reduce typing while hiding an assumption, prefer the more explicit design.

4. Pure helpers

Extract sorting, filtering, formatting, validation, and state transitions into pure functions when doing so improves testability and reduces component noise. A pure helper receives its inputs and returns its result without mutating shared state or relying on hidden runtime behavior. That makes edge cases easier to exercise independently from rendering.

Do not extract a helper solely because a function is a few lines long. Extract it when the rule has a meaningful name, can be reasoned about independently, or is likely to be reused.

Decision rule: Use pure helpers deliberately when they make a contract or invariant easier to prove. If they only reduce typing while hiding an assumption, prefer the more explicit design.

5. Dependency direction

Higher-level feature code should depend on stable helper and component contracts, not on deep internals from sibling features. Deep imports make a feature's implementation part of another feature's API, so a local refactor can unexpectedly spread across the codebase.

The useful test is to ask which direction a change should travel. A feature can use a stable shared contract; it should not need to know how another feature happens to organize its private files.

Decision rule: Use dependency direction deliberately when it makes a contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

6. Incremental design

Start with the simplest structure that supports the first vertical slice. A vertical slice carries one useful path through the relevant UI, data access, and behavior. After a second use case appears, look for duplication or a boundary that has become real. Refactor at that point, when the abstraction is supported by evidence, instead of designing a framework for problems that may never arrive.

Decision rule: Use incremental design deliberately when it makes a contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

Worked example

Consider a timed machine-coding exercise that must stay readable, accessible, testable, and easy to extend while you are under interview pressure. Begin by writing the requirement in one sentence. Then list the input and output contracts and identify which concept above owns each failure mode.

The key move is separation of responsibility. Parsing or input validation belongs at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. These boundaries do not require a large architecture; they require each rule to have an identifiable owner. Mixing the concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about.

ts
export async function handleRequest(input: unknown) {
  const command = parseCommand(input);
  const result = await service.execute(command);
  return toHttpResponse(result);
}

This handler expresses a useful flow without pretending to show every implementation detail. An unknown input is parsed before the service sees it, the service operates on a command rather than raw transport data, and the result is translated back into an HTTP response at the outer boundary. Each function therefore has a contract that can be tested independently.

Walk through at least four cases: the normal path; an empty or missing value; a duplicate, retry, or concurrent path where that concern applies; and a dependency failure. For every case, state which layer detects the problem and what the caller observes. For example, malformed input should be rejected at the boundary, while a failure from a dependency should be represented in the boundary's response without leaking transport details into the domain. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failures, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality workloads. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, account for loading, empty, error, stale, and success states. When it involves security, assume that the client can be modified and that network input is untrusted. A clean folder structure cannot compensate for an undefined contract at any of these boundaries.

Guided lab

Create the folder, component, and data-flow skeleton for a small issue tracker. Implement only list loading and item rendering first. Add one feature after that, and refactor only when duplication or coupling becomes concrete rather than hypothetical.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List the input, output, and error contracts before implementing.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

The lab is intentionally incremental. Its purpose is to make you practice the point at which an abstraction becomes justified, not to reward the largest initial folder tree.

Edge cases and failure modes

  • Feature boundaries: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Layer restraint: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • API adapters: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Pure helpers: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Dependency direction: Test absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.

These categories overlap because the same input can expose more than one design problem. The important debugging question is still specific: which boundary owns the invariant, and what behavior should the caller see when that invariant is violated?

Common mistakes and debugging

  • Solving the example instead of the requirement. A copied pattern may be syntactically correct while being architecturally wrong for the actual constraints.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values. These choices suppress evidence instead of resolving the uncertainty.
  • Testing only the happy path and discovering the real contracts only during integration.
  • Optimizing before measuring, or choosing a scalable mechanism without a scale requirement.
  • Treating client-side behavior as a substitute for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the layer that owns the problem rather than adding a downstream patch. If the symptom appears in the UI, the original defect may still be in the adapter, validation, or data model.

Interview questions

  1. What problem do feature boundaries solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does layer restraint solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem do API adapters solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem do pure helpers solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does dependency direction solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Project Structure, Boundaries, and Incremental Architecture to another developer in five minutes. Include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or under stricter reliability requirements.

References

Reader page: /machine-coding/lesson/237/project-structure-boundaries-and-incremental-architecture