1. 09Feature modules are units of change
  2. 10The parts of a feature
  3. 11Making a feature ready to build
  4. 12Building a feature from idea to implementation
  5. 13Testing a feature without the host
  6. 14The application is the composer

Building product features

10

The parts of a feature

A feature module groups UI, frontend-facing contracts, and a runnable demo while keeping production integrations outside.

By Eyal Ellenbogen August 19, 2026 3 min read

A feature module succeeds when its internal anatomy is predictable. When every product workflow invents its own file structure, navigating our codebase requires continuous re-orientation.

A clean feature boundary divides naturally into three co-located responsibilities, while production integrations stay outside the module.

The three parts

Inside the feature directory, files group by responsibility rather than technical file type:

  1. ui: The feature's views, layouts, and presentational components. It renders prepared display models and emits user intent, while state coordination drives the port outside markup.
  2. api: The contract describing what the feature needs from the outside world. It contains the feature-owned port interface and the data shapes crossing that boundary.
  3. demo: A standalone harness running the feature with controlled mock data. It lets teams test and review UI states without touching live backend infrastructure.

Feature module

ui

Views & layouts

api

Port contract

demo

Mock harness

Adapter (outside the module)

Implements the port against real services

The feature module co-locates ui, api, and demo. A production adapter lives outside the boundary and satisfies the port; the feature never imports it directly.

The golden rule is dependency direction: feature code never imports backend clients, HTTP libraries, or production adapters. The feature states what it needs; outside code satisfies that contract.

Keep views focused on presentation

Even with a dedicated port contract, components can collapse if templates take on workflow coordination. If a view calls the port directly in an async effect, tracks retry counters, and evaluates permissions like user.role === 'admin' inline, presentation markup becomes coupled to business rules.

We keep that division clean inside ui. Visual components should receive finished models—ready-to-render strings, arrays, and boolean flags like canManage: boolean. State coordination (whether written as a custom hook, a producer, or a state machine) manages pending requests and drives the port, while the view simply renders the outcome and reports user interactions. This keeps visual adjustments safe from state regressions and allows the view to mount in a test harness with plain mock data.

Ports describe what the UI needs

A frontend view should not bind directly to backend response payloads. This is the essence of Ports and Adapters (Hexagonal Architecture) applied to the frontend: the feature defines an explicit contract at its edge, expressed in UI-facing terms:

export interface EditMemberPort {
  loadMember(): Promise<Member>;
  saveMember(change: MemberChange): Promise<void>;
}

This contract reflects what the screen needs to render, not how the database stores it. The UI never parses query parameters, handles raw HTTP status codes, or imports entity schemas from an external API package.

Adapters isolate production realities

Production applications must eventually talk to real services. That translation belongs in an integration adapter located outside the feature module:

export function createBackendEditMemberAdapter(
  client: MembersApiClient,
  memberId: string,
): EditMemberPort {
  return {
    async loadMember(): Promise<Member> {
      const response = await client.getMember(memberId);
      return toMember(response);
    },

    async saveMember(change: MemberChange): Promise<void> {
      await client.updateMember(memberId, toUpdateRequest(change));
    },
  };
}

The adapter absorbs backend volatility. If an endpoint URL changes, a field renames, or an API migrates from REST to GraphQL, only the adapter changes. The feature's UI and internal contracts remain untouched.

The demo proves the boundary

The demo directory is the practical test of the feature boundary. It pairs the feature UI with an in-memory mock implementation of the port:

export const mockEditMemberPort: EditMemberPort = {
  async loadMember() {
    return { name: 'Alice', role: 'Admin' };
  },
  async saveMember() {},
};

If we cannot run the feature in isolation without logging into a VPN, seeding a database, or starting five local microservices, the boundary has failed.

Module boundary

Inside feature
Presentational views, port contracts, mock demo harness
Outside feature
Production adapters, network clients, host routing

When ui, api, and demo stay together and production plumbing stays outside, a feature becomes a true unit of change: fast to develop, easy to test, and resilient to backend shifts.