Clean Frontend Architecture: How to Organize React Code by Business Domains

Minimalist image of an AI-generated model of a house to illustrate the post on hexagonal architecture in Orbitant's frontend.

You start a frontend project with organized folders. Three months later: where does this component go? Which file has that validation? Why does this component have 500 lines mixing API calls with business logic?

The symptoms are always the same: messy folders, business logic mixed with UI, scattered APIs with no pattern, and impossible testing because you need to mock dozens of dependencies just to test a single validation.

The cost: onboarding that drags on for weeks, changes with unpredictable side effects, and validations duplicated across five different places. The solution comes from the backend but works perfectly in the frontend: hexagonal architecture.

What is hexagonal architecture and why it works in frontend

Hexagonal architecture (also known as Clean Architecture or Ports and Adapters) structures code around business rules, not frameworks. Your business logic is not dependent on React, Vue, or Angular.

The golden rule: dependencies always point inward. Concentric circles where the centre is your domain (pure business rules), then application (use cases), infrastructure (databases, APIs), and finally presentation (UI). Each layer only knows about the layers immediately below it.

Why does it work in the frontend? It separates what your application does (domain), how it does it (infrastructure), and how it shows it to users (presentation). You switch from React to Vue by only touching the presentation. You change from REST to GraphQL by only modifying the infrastructure. Furthermore, you test business logic without launching a browser.

The house metaphor: understanding the 4 layers without jargon

To understand how this architecture works, think about the four phases of building a house. Each phase has a direct equivalent in our code.

1. Blueprint (Domain): The blueprint is what you define first, before building anything. It specifies how many rooms there are, what characteristics they have, and what rules they must follow.

  • In code, this is your domain layer: entities and pure business rules. A sofa must have three seats, be made of leather or fabric, and fit in the room. This is pure JavaScript or TypeScript, without importing external libraries.

2. Instructions (Application): Construction instructions define what must happen and in what order, but don’t specify which tool to use. “Fasten the panel to the structure using a tool that can drive fasteners” – it doesn’t matter if you use a hammer or a drill.

  • In code, these are your use cases: create furniture, validate it, save it. The application layer orchestrates the flow but doesn’t know if data goes to MongoDB, PostgreSQL, or LocalStorage.

3. Tools & Materials (Infrastructure): This is where real tools appear: hammers, drills, wood, and screws.

  • In your application, this is the layer that communicates with the outside world: HTTP clients for APIs, database adapters, and storage services. This layer implements the interfaces that the application layer defines, but can be entirely changed without affecting business logic.

4. Builders (Presentation): The workers who execute instructions and make the house visible. In code, these are your React, Vue, or whatever components you use.

  • This layer interacts directly with the user, receives inputs, shows outputs, and calls use cases to execute actions. It’s the only layer that “sees” the UI framework.

From living room to code: practical example with validations

We design a living room: a sofa, a coffee table, and a TV. The sofa must have 3 seats, be leather or fabric, and be blue or yellow. The table must fit in the space. The TV: minimum 55 inches and 4K.

Pure business rules that don’t depend on how you store data or what UI you use:

// Domain layer - entities.ts
export type Furniture = {
  id: string;
  type: 'sofa' | 'coffee_table' | 'tv';
  brand: string;
  model: string;
  dimensions: Dimension;
  color: string;
  material: string;
  price?: number;
  image: string;
  mountingType: 'wall' | 'floor';
  createdAt: Date;
  updatedAt: Date;
};

And here are the validations in the domain services layer:

// Domain layer - services.ts
export const isValidSofa = (params: SofaValidationParams): boolean => {
  const { furniture, seatCount, roomDimensions, doorways } = params;

  // Rule 1: Must have 3 seats
  if (seatCount !== 3) {
    return false;
  }

  // Rule 2: Material must be leather or fabric
  const validMaterials = ['leather', 'fabric'];
  if (!validMaterials.includes(furniture.material.toLowerCase())) {
    return false;
  }

  // Rule 3: Color must be blue or yellow
  const validColors = ['blue', 'yellow'];
  const furnitureColor = furniture.color.toLowerCase();
  if (!validColors.some(color => furnitureColor.includes(color))) {
    return false;
  }

  // Rule 4: Must fit inside the room dimensions
  if (
    furniture.dimensions.width > roomDimensions.width ||
    furniture.dimensions.depth > roomDimensions.depth
  ) {
    return false;
  }

  // Rule 5: Cannot block doorways
  for (const doorway of doorways) {
    if (
      furniture.dimensions.width >= doorway.width &&
      furniture.dimensions.depth >= doorway.height
    ) {
      return false;
    }
  }

  return true;
};

See any reference to React, databases, or UI libraries? No. These validations run in browser, server, unit test, or Lambda. They’re technology-independent.

That’s why they CANNOT depend on external libraries. The day you want this logic in React Native, or migrate to Vue, or create a CLI, your domain code stays intact.

How this translates to a real React project

Structure by business domains, not by file type. Instead of components/, services/, utils/, you have living-room/, kitchen/, bedroom/.

Within each domain, the four layers have their own subfolder:

living-room/
├── domain/
│   ├── entities.ts           // Pure types
│   ├── repository.ts         // Interfaces (contracts)
│   └── services.ts           // Validations and rules
├── application/
│   └── use-cases.ts          // Create furniture, validate, etc.
├── infrastructure/
│   └── repositories.ts       // Real implementation (API/DB)
└── presentation/
    └── component.tsx         // React components

The Domain layer contains only pure TypeScript. In entities.ts you define data types, in repository.ts you define interfaces (contracts) that specify what operations must exist without saying how, and in services.ts you write validation functions and business rules.

The Application layer contains use cases in use-cases.ts. For example, createFurniture receives a repository as a dependency and applies domain validations before saving. This layer orchestrates the flow but doesn’t know if you’re using MongoDB, PostgreSQL, or LocalStorage. It only knows there’s a repository that fulfills the contract defined in the domain.

The Infrastructure layer has real implementations in repositories.ts. This is where HTTP calls appear, database clients, and LocalStorage access. This layer implements the interfaces defined in the domain.

The Presentation layer contains your React components in component.tsx. Here you use hooks, state, effects, and everything specific to React. Components call use cases to execute actions and show results to the user.

The key advantage: you switch from React to Vue by only touching the presentation. You change from REST to GraphQL by only modifying the infrastructure. Domain and use cases remain intact.

Real case at Orbitant: how it works in Verifactu

At Orbitant, we applied it in MisFacturas, a Verifactu-compliant electronic invoicing project. Before writing code, we mapped domain entities and services.

Identified domains: company, concept, contact, document. Immediate benefit: any developer opens domain/ and understands the business in five minutes.

The most interesting case: developing without a backend. We started with LocalStorage. We defined the repository interface and created an implementation that was saved in the browser.

When the backend was ready, we created a new implementation that called the API. We changed dependency injection in one place, and the entire application used the real backend. Not a single line of business logic was modified. Only infrastructure.

This is hexagonal architecture in action.

Advantages and disadvantages: when to use hexagonal architecture in frontend

Real advantages:

  • Extremely simple testing: isolated layers. You test validations without React, without mocking APIs, without mounting components. You import the function and test it with pure data.
  • Changing technologies is trivial. Preact instead of React? Rewrite the presentation. Axios instead of Fetch? Change infrastructure. Redis cache? New repository implementation.
  • Fast onboarding: you open domain/ and understand the business in minutes. Clear, predictable structure.
  • Long-term maintainability: business rules in one place, no duplicates, surgical changes.

Honest disadvantages:

  • Requires initial planning. You can’t just start coding components directly. You need to think about domains, entities, and use cases.
  • More files and folders. Instead of one component with everything, you have four files in four folders. Can be overwhelming at first.
  • Needs team discipline. If someone imports the HTTP client directly into a component, they break the architecture. Requires code reviews.
  • Can be overkill for small apps. A three-screen landing page doesn’t need this.

When YES? Enterprise projects. Distributed teams. Long-lived applications. Complex business logic.

When NO? Quick MVPs. Landing pages. Prototypes that will be rewritten. Trivial applications without relevant logic.

Conclusion: architecture as investment, not expense

Hexagonal architecture solves real production problems: unmanageable code, impossible testing, painful migrations.

Initial cost recovers quickly. The first weeks of maintenance compensate for the investment. New developers contribute in days. Technology changes go from month-long projects to hour-long tasks.

That’s why it’s our standard for complex frontend projects. Not for everything, but yes for applications that will grow.

If you want to dive deeper: Clean Architecture by Robert C. Martin. Though focused on the backend, the principles apply perfectly when you understand that React is just an implementation detail.

The question isn’t whether your project needs architecture. It’s whether you wish to define it consciously from the start, or let it emerge chaotically.

Frequently Asked Questions

How do I handle pages that use multiple domains?

A page component (Next.js, for example) can combine multiple domains. Presentation orchestrates. Your page imports use cases from company, contact, and document simultaneously. Each domain stays isolated, presentation composes them.

Can I use one domain inside another?

Yes. Example: document might need concepts from company. But if a concept is used by many domains, better move it to shared/ or common/. You avoid circular dependencies. Rule: prefer composition in upper layers rather than dependency between domains.

How do I use this with AI and tools like Cursor?

Perfect for AI: predictable, repetitive structure. Create prompts that generate complete domains. In our team, David created a script that generates all layers of a domain by just specifying entities. With Cursor, define rules in .cursorrules to respect structure and dependency conventions.

What about reusable tools like RabbitMQ or FTP clients?

Repositories are interchangeable. RabbitMQ client that multiple domains use: you define a repository interface in each domain according to its needs, and the infrastructure implementation reuses the same client. The important part: the interface is defined from domain needs, not from tool capabilities. The tool adapts to the domain.

Follow our journey building AI agents from the inside