Phase 6 · Advanced & ProfessionalModule 37~50 min read

Patterns, Functional Design & Architecture

Organize growing applications with proven patterns, functional techniques, and clear boundaries.

What you'll learn

Architecture is the arrangement of decisions and dependencies. Patterns provide vocabulary, functional design makes data flow explicit, and boundaries keep infrastructure from dominating domain behavior.

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

  • Apply patterns from a concrete pressure
  • Use composition and dependency injection
  • Separate domain, application, and infrastructure concerns

Core mental model

Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.

ConceptWhat it meansDecision rule
CompositionBuild behavior from cooperating capabilitiesPrefer to deep inheritance
Dependency inversionPolicy depends on contracts, not infrastructure detailsInject gateways at application boundaries
State transitionPure function maps old state and event to new stateUse when UI or workflow state becomes complex

Professional workflow

Build the behavior in small, observable steps. Each step should leave something you can inspect or test.

  1. Describe the application architecture boundary: inputs, outputs, state, timing, and expected failures.
  2. Implement the smallest correct path with names that expose intent.
  3. Add edge cases and failure handling before introducing abstractions.
  4. Verify behavior with realistic data and one deliberately adversarial example.
  5. Refactor only after the observable behavior is protected.

Make behavior observable

Before optimizing or abstracting, make inputs, outputs, state changes, timing, and failure paths visible. JavaScript becomes much easier to reason about when hidden work is exposed.

Guided code lab

Inject a repository contract

The use case owns policy; storage is supplied from outside.

complete-lesson.js
export function createCompleteLesson({ progressRepository, clock }) {
  return async function completeLesson(userId, lessonId) {
    const completedAt = clock.now();
    await progressRepository.save({ userId, lessonId, completedAt });
    return { lessonId, completedAt };
  };
}

Model state as a pure transition

Events and state are data, so every transition can be tested without a browser.

reducer.js
function progressReducer(state, event) {
  switch (event.type) {
    case "lesson_completed":
      return { ...state, completed: [...state.completed, event.lessonId] };
    case "reset":
      return { ...state, completed: [] };
    default:
      return state;
  }
}

Production practice

Contract

Write down the application architecture quality target, ownership boundary, evidence, and acceptable tradeoffs.

Verification

Use repeatable automated checks plus a realistic end-to-end scenario; include failure and recovery behavior.

Operations

Track a small set of user-centered signals and revisit the decision when evidence changes.

Common failure mode

Adding factories, repositories, observers, and layers before a concrete change pressure creates ceremony without protecting a real decision.

Independent workshop

Refactor the course application around domain use cases and injected gateways.

Your finished workshop must include:

  • A dependency diagram
  • Pure state transitions
  • One pattern justified by a real maintenance pressure

Definition of done

Demonstrate the happy path and at least two edge cases, keep responsibilities separated, and add a short note explaining one design choice.

Recap & quick check

Key takeaways

  • Patterns respond to pressures
  • Composition keeps capabilities flexible
  • Dependency inversion protects policy
  • Pure transitions make state testable

Quick check

1. What should domain policy depend on?

2. When should a pattern be introduced?

3. What does a reducer return?

Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.