Skip to content

Quick Start

In this section, we’ll cover all the basic LAD patterns.

This article will be enough to start getting value from using this pattern.

Further articles in the guides section will explore individual aspects of working with LAD.

What projects can be built with LAD

LAD is designed to work with major web frameworks: vue, react, svelte, solid.

In simple modifications, it will be useful even in the most basic projects.

The only thing is that LAD is not designed for library development. But you can always take some patterns if they are useful to you.

LAD modifications

The architecture is presented in 4 modifications:

  1. For small projects (up to 12 person/months of development)
  2. For medium projects (team up to 5-6 frontend developers)
  3. For large projects (more than one team)
  4. For a team working on multiple projects

This guide will describe the first 2 modifications. 3-4 will be described in other articles in the future.

What LAD consists of

For an external observer, these are just folders and files. In reality, these are abstractions and architectural boundaries.

We’ll talk about this in more detail later. But for now, nothing prevents you from thinking of them as folders and files.

The first thing LAD starts with is layer separation.

Main layer separation

As in most architectures, in LAD at the top level, code is organized by layers.

There are 4 top-level layers in total.

  • Directorysrc/
    • Directoryroot/
    • Directoryfeatures/
    • Directoryservices/
    • Directoryshared/
Differences from FSD
  • In LAD shared unlike FSD shared can contain business logic.
  • services are very similar to entities, but there are no restrictions on the semantics of “business object”
  • features are large chunks of application functionality. Essentially, these are the pages widgets features entities layers in one.
  • There is no separate pages layer. Page code is located in features. If pages contain multiple features, then composition is located in root
  • We call slices modules, and segments groups. Submodules are also allowed.

Root


The entry point layer. Here the project is launched, global configuration occurs, and features are connected into a single application.

The internal structure of root is not standardized and heavily depends on the application.

The layer where the most frequently changing logic should be located.

Examples of code that can be here:
  • app.tsx root application component
  • root-layout.tsx common layout for the entire application
  • root-header.tsx header for the entire application. Can delegate tasks to components from features
  • global.css global styles
  • router.tsx react-router initialization
  • providers.tsx component that uses global react providers
export function Providers({ children }: { children: React.ReactNode }) {
return (
<StoreProvider store={store}>
<QueryProvider client={queryClient}>{children}</QueryProvider>
</StoreProvider>
);
}
  • In next.js app remains itself, just try to move most of the logic to features

Features


The main layer. Most of the application code should be located here.

Each folder in the features layer is an implementation of large independent chunks of functionality

Example features for a task planner

  • Directorysrc/
    • Directoryroot/
    • Directoryfeatures/
      • Directoryauth/
      • Directorytask-list/
      • Directorysidebar/
      • Directorysettings/

Interaction restriction with root

This extremely important restriction gives us the following:

  • root contains frequently changing things. Dependency on frequently changing code makes features unreliable sdp
  • Import from root is almost always a circular dependency. Circular dependencies are always bad, both for the compiler and for the brain. adp

Feature structure

Each feature is essentially a module!

Since modules in LAD can be large, their structure is described in detail below.

Now it’s important to understand that a feature can contain everything: display components, logic, infrastructure, and pages.

The main criterion is that all these things should have high semantic cohesion.

Feature interaction

There’s a difference between “small” and “medium” modifications here.

Medium modification
  • Directorysrc/
    • Directoryroot/
    • Directoryfeatures/
      • Directoryauth/
      • Directorytask-list/ // from task-list you cannot import auth

This restriction is very important in medium and large projects to be able to consider features as independent blocks.

  • This greatly increases understandability. Since you can consider a feature in isolation from others
  • Increases reliability. Since changes in one feature are less likely to break another
  • Protection from circular dependencies.

In reality, completely isolated features are rare. Therefore, for feature interaction with the prohibition on direct import, loose coupling patterns or dependency inversion are used.

Sounds complicated, but in fact it’s: connection through shared state, slots, render props, events, context, and dependency injection.

More about DI

This is the most complex part of LAD. For this reason, in the “small” modification, it’s impractical to impose this restriction.

Small modification

In the small modification, direct import is allowed. But this doesn’t cancel that there should be as few of these imports as possible!

The fewer connections between features there are, the better. Ideally, there should be none at all.

Services

Layer of reusable business modules. Can store not only logic but also presentation.

Like a feature, each service is a self-sufficient module. But unlike a feature, it doesn’t implement application functionality, but helps features do their work.

Most often modules in services are needed if there’s a large amount of logic reused between features.

Example services for a task planner

  • Directorysrc/
    • Directoryroot/
    • Directoryfeatures/
      • Directoryauth/
      • Directorytask-list/
      • Directorymanage-settings/
    • Directoryservices/
      • Directorysession/
      • Directorysettings/

In this case, session most likely contains session storage used in most other features. And settings stores settings that are edited in the manage-settings feature and used in task-list.

Interaction restriction with root and features

Just as features cannot import root, services cannot import features and root.

This is done for the same reasons. But for services, this is even more important. They are most often reused in multiple places.

Dependency on more frequently changing features would make services unstable and endanger several other features at once.

Service interaction

There’s no unambiguous solution here. Basically, I allow service interaction with each other.

Since most often dependency inversion at this level causes a lot of complications.

But if problems appear, the same restriction as for features is introduced.

Shared

Application core layer. Here are things that are used throughout the application.

Try to place only the following in shared:

  • Global business types. Types rarely change and don’t cause bugs
  • Global infrastructure. Store instances, notifications, internationalization, theme Usually such infrastructure is used widely and changes rarely
  • Global constants linking the application. For example, routing constants and .env reading result
Important exceptions:

Essentially uikit and api shouldn’t be in shared.

These are extremely frequently changing modules. But all attempts to move them from shared failed.

So know that uikit and api are eternal sources of bugs. And treat them accordingly.

The shared structure is not rigidly standardized, but always similar.

Example of standard shared

everything listed below is optional and may not be needed in your case

  • Directorysrc/
    • Directoryshared/
      • Directorydomain/ // global business types. Needed for infrastructure and entire application work
        • ids.ts
        • events.ts
        • session.ts
        • user.ts
      • Directoryui/ // application uikit
        • Directorykit/
          • button.tsx
        • Directorytable/ // reusable table component
      • Directoryapi/ // usually automatically generated api instance
        • api-instance.ts // axios instance
        • generated.ts
      • Directorymodel/ // working with global data
        • routes.ts // routing constants
        • config.ts // getting access to .env
        • store.ts // redux instance
      • Directorylib/ // Global infrastructure and helpers
        • Directorynotifications/
        • Directoryi18n/
        • Directoryreact/
          • use-mutation-observer.ts
        • date.ts

domain, model, ui, lib

All shared code is divided into standard groups:

  • domain: The most important business types and rules. This group should be isolated and not depend on anyone
  • model: Working with global state
  • ui: Global components
  • lib: global infrastructure modules

Interaction restriction with root, features and services

shared is the layer where root logic is located. It cannot directly work with any other layer.

While it can and will be imported by all other layers.

This rule is extremely important and should not be violated. shared is a dangerous layer. You always need to be on guard with it)

Layer schema

”Small” modification

Features Layer

🚀 App Entry Point

📦 Feature 1

📦 Feature 2

📦 Feature 3

🔧 Shared Core Layer

”Medium” modification

Services Layer

Features Layer

🚀 App Entry Point

📦 Feature 1

📦 Feature 2

📦 Feature 3

⚙️ Service 1

⚙️ Service 2

🔧 Shared Core Layer

Module structure


features and services - in LAD contain modules. But if you wish, you can create modules in root and shared as well.

Also, LAD supports the concept of sub-modules, so modules in LAD can be very large.

Essentially, modules in LAD can be considered as mini-applications that have their own architecture.

Module evolution stages

We’re against overhead, so modules have evolution stages, from simple to complex.

Always when creating modules start with the simplest stages, and then refactor if required.

In our experience, this is the most effective approach! Excessive architecture is worse than its lack.

Stage 1: Single file module

Yes, the simplest modules can consist of only one file!

Here’s an example of the todo-list feature at the very first stages

import { useState } from "react";
export function TodoListPage() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodo = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, { id: Date.now(), text: input, done: false }]);
setInput("");
};
const toggleTodo = (id) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Todo</h1>
<form onSubmit={addTodo} className="flex mb-4">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 border p-2 rounded-l"
placeholder="Add todo..."
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 rounded-r hover:bg-blue-600"
>
Add
</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id} className="flex items-center mb-2">
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
className="mr-2"
/>
<span
className={`flex-1 ${
todo.done ? "line-through text-gray-400" : ""
}`}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700"
>
×
</button>
</li>
))}
</ul>
{todos.length > 0 && (
<div className="mt-4 text-sm text-gray-500">
{todos.filter((t) => !t.done).length} items left
</div>
)}
</div>
);
}

Most often modules quickly outgrow this stage. But sometimes they don’t!

Working with a file up to 400 lines is acceptable.

At the same time, the development and refactoring speed of such a module is higher than a module of 20 files with 20 lines each.

Don’t neglect this approach, especially at the prototyping stage.

Stage 2: Flat module

Your file has become larger than 400 lines and uncomfortable. What do we extract components/hooks/ui/model?

Not yet, it’s too early!

There’s nothing worse than a folder with one file.

The second evolution stage is creating a flat structure of heterogeneous modules.

Here’s an example of the todo-list feature at the second stage:

  • Directorytodo-list/
    • todo-list-page.tsx
    • use-todo-list.tsx
    • api.tsx
    • use-intersection-observer.tsx
    • index.ts

Here we simply divided the code into functions. And then moved them to separate files. We name files according to their content. We avoid names like hooks components.

This number is derived empirically and is related to the concept of Miller’s wallet.

Public api

At this stage, it’s not clear which code is intended for external use and which should remain internal.

Therefore, we add public-api.

This is an index.ts file where you re-export (sometimes with name changes) externally available elements.

export { TodoListPage } from "./todo-list-page";
For those with next.js

If there are no side effects in index files, then tree-shaking works correctly in vite.

With next.js, we’ve encountered problems. So a different approach with public-api is often used there.

All private files start with _, and public ones without. Thus, it’s not necessary to create only one public file.

  • Directorytodo-list/
    • todo-list-page.tsx
    • _use-todo-list.tsx
    • _api.tsx
    • _use-intersection-observer.tsx

Explicit dependencies (experimental)

When modules interact with each other, it’s often extremely difficult to track all module dependencies on other modules.

Therefore, all dependencies used inside can be re-exported through a special deps.ts file.

This is especially important for services and features if cross imports are allowed (from feature to feature, from service to service)

Example:

  • Directoryfeatures/
    • Directorysettings/
    • Directoryauth/
    • Directorytodo-list/
      • index.ts
      • deps.ts
      • todo-list-page.tsx
      • use-todo-list.tsx
      • create-todo-form.tsx

deps.tsx

export { useSession } from "@/features/auth";
export { useSettings } from "@/features/settings";

todo-list-page.tsx

import { useSession, useSettings } from "./deps";

Sub modules

Essentially, when we made such division, we got a module that consists of several single-file modules.

But they don’t have to be single-file. Any child module can be at any evolution stage.

  • Directorytodo-list/
    • index.ts
    • todo-list-page.tsx
    • use-todo-list.tsx
    • Directorycreate-todo-form/
      • index.ts
      • create-todo-form.tsx
      • use-create-todo.tsx

Nesting dangers

Thanks to submodules, you can stay at the second level for a very long time.

But deep nesting is not as good as it seems. Deep tree structures are complex to understand. Homogeneous lists are much more comfortable to read (the features layer is just an example of a homogeneous list).

Therefore, most often to overcome the limitation of ~6 elements, it’s better to prefer creating groups over submodules.

Stage 3: Grouped module

As mentioned above, if for heterogeneous folders < 6 elements is comfortable. For homogeneous folders, this number sharply increases (up to 20 elements. Heavily depends on homogeneity).

Therefore, we can divide submodules into groups, thus greatly increasing the allowable module size.

What is a group

A group is a combination of several modules based on a common feature:

Group examples:

  • components
  • hooks
  • services
  • features
  • ui
  • model
  • lib
  • api

By the way, all layers are also groups.😉

Standard groups

There are quite successful groups that have shown themselves well:

Group: ui

Essentially, this is a combination of all components, rarely hooks, that are entirely responsible for display. And don’t carry complex logic.

Examples:

  • todo-list-page.tsx
  • todo-card.tsx
  • todo-form.tsx
  • use-render-arrows.tsx
Group: model

The group where the main work with data lies.

If you’re on pure react, here lie hooks that manipulate data apart from display.

If you have a state manager, then all the logic of working with the state manager will lie here.

Examples:

  • use-todo-list.ts
  • todo-list.slice.ts
  • todo-list-store.ts
  • todo-item.ts
Group: lib

The group where infrastructure code is located. This code most often provides more convenient wrappers over browser api and libraries. Or simply simplifies routine tasks.

Examples:

  • use-mutation-observer.ts
  • date.ts
Group: api

Group for code working with api and contract types.

Group: domain

If the logic in model becomes very complex.

Then code describing the most important business processes:

  • Discount calculation
  • Vacation calculation
  • Progress getting
  • Coordinate calculation when moving an element on a map

Can be moved as pure functions to the domain group.

Also in domain are all types over which these pure functions perform manipulations.

Examples:

  • map-node.ts
  • get-intersections.ts
  • compute-next-lesson.ts
Group: view-model

In some cases, the module contains a large amount of logic that processes user input.

Usually this happens if dnd or animations are implemented.

In such case, this code can be moved to a separate view-model group.

Example:

  • use-dnd.tsx
  • use-animation.tsx

Nested groups

Inside a group, you can group modules by other features as well.

This works exactly the same as with submodules. Just don’t forget that excessive depth is inconvenient.

Example:

  • Directoryui/
    • Directoryfields/
      • file-field.tsx
      • text-field.tsx
      • select-field.tsx
      • date-field.tsx
    • create-user-form.tsx
    • update-user-form.tsx

Example module with groups

  • Directorytodo-list/
    • index.ts
    • api.ts
    • Directorymodel/
      • todo-item.ts
      • use-create-todo.ts
      • use-update-todo.ts
    • Directoryui/
      • Directorypages/
        • todo-list-page.tsx
        • todo-details-page.tsx
      • Directoryfields/
        • file-field.tsx
        • text-field.tsx
        • select-field.tsx
        • date-field.tsx
      • create-todo-form.tsx
      • todo-details-form.tsx
      • todo-item.module.css
      • todo-item.tsx

Groups and submodules

The appearance of groups doesn’t cancel submodules. Sometimes instead of dividing code into groups, it’s better to split all code into several submodules.

  • Directorytodo-list/
    • index.ts
    • api.ts
    • Directoryui/
      • file-field.tsx
      • text-field.tsx
      • select-field.tsx
      • date-field.tsx
    • Directorytodo-list/
      • index.ts
      • todo-list-page.tsx
      • create-todo-form.tsx
      • use-create-todo.ts
      • todo-item.tsx
    • Directorytodo-details/
      • index.ts
      • todo-details-page.tsx
      • todo-details-form.tsx
      • use-update-todo.ts

When to move to the next stage?

At this stage, you can develop modules of any size.

But there’s an important problem here - connections between submodules are chaotic, and it can be very difficult to understand them.

If you’ve encountered this problem, the next stage will help you:

Stage 4: Module with compose

This is a complex but extremely powerful pattern for fighting chaotic dependencies.

Its closest analogue is the DIP principle from SOLID.

The main essence of this pattern is to remove dependencies between submodules in model and ui.

For this, loose coupling tools are used. (for ui slots and render props, for model events, DI, or simple connection through parameters)

After we get a set of independent elements, all this is combined in special mediator components.

For them, I usually create a separate compose group. Connections between components in compose are allowed.

Example:

  • Directorytodo-list/
    • index.ts
    • api.ts
    • Directorycompose/
      • todo-list-page.tsx
      • todo-details-page.tsx
      • create-todo-form.tsx
      • todo-details-form.tsx
    • Directorydomain/
      • todo-item.ts
    • Directorymodel/
      • use-todo-list.ts
      • use-delete-todo.ts
      • use-create-todo.ts
      • use-update-todo.ts
    • Directoryui/
      • Directoryfields/
        • file-field.tsx
        • text-field.tsx
        • select-field.tsx
        • date-field.tsx
      • todo-page-layout.tsx
      • todo-item.tsx
      • common-fields.tsx
      • update-button.tsx
      • delete-button.tsx

Then todo-list-page.tsx would look something like this:

export function TodoListPage() {
const todoList = useTodoList();
const deleteTodo = useDeleteTodo(todoList);
const createTodo = useCreateTodo(todoList);
const updateTodo = useUpdateTodo(todoList);
return (
<TodoPageLayout
createForm={<CreateTodoForm createTodo={createTodo} />}
todos={todoList.list.map((item) => (
<TodoItem key={item.id} item={item}>
<UpdateButton onClick={updateTodo.bind(item)} />
<DeleteButton onClick={deleteTodo.bind(item)} />
</TodoItem>
))}
/>
);
}
Where you can read more

Documentation is still actively being developed. Later there will be a separate article about this pattern. Now you can study the following materials:

Stage 4 limitations

The main limitations here are technical. Not all tools support loose coupling. And not in all situations does this give the needed performance.

But in most situations, this approach helps greatly reduce module complexity!

Conclusions on module evolution

Maybe you’re already confused by all the module variants presented here.

This is normal, such flexibility slightly complicates entry.

Upon deeper consideration, it turns out that these are all variations around three concepts: module group and public-api.

In fact, this approach both standardizes approaches. And provides flexibility.

As your modules become more complex, you can build convenient and maintainable code from module and group building blocks.

While not spending time on constant overhead from inconvenient rituals and patterns.

What’s next?

Using the advice from this guide, you can already start using LAD now.

If you want to understand how this all actually works. Come read the advanced part of the documentation.

Now this part is just starting to develop, but over time more and more articles will appear there, revealing concepts and patterns.