Skip to main content

ADR 0003: Standardized Frontend Layout

Date: 2025-08-09

Status: Accepted

Context

With multiple frontend applications planned (e.g., shell-app, various micro-frontends), we need a consistent and predictable directory structure. A standardized layout is essential for:

  • Reducing Cognitive Load: Developers can navigate any frontend application and immediately understand where to find different types of logic (UI components, state, API calls, etc.).
  • Promoting Best Practices: The directory structure itself can guide developers toward a scalable architecture, such as feature-slicing, which co-locates related logic.
  • Simplifying Tooling: Consistency makes it easier to configure and apply tooling like linters, generators, and test runners across all frontend projects.

ADR 0003: Standardized Frontend Layout

Date: 2025-09-07

Status: Accepted

Context

With the evolution of our frontend strategy, we recognize the need for a highly flexible and developer-centric approach to application development. Our goal is to foster a "1000% focus on developer experience," enabling teams to choose the most suitable tools and frameworks for their specific needs without being constrained by a rigid, opinionated structure. This necessitates a framework-agnostic approach to frontend layout, promoting consistency through principles rather than prescriptive technologies.

Furthermore, the shell application's role is being redefined to be exceptionally minimalist. It will no longer enforce specific UI elements like menus but will strictly provide core authentication and context-switching functionalities: login, logout, user-switch, and tenant-switch. This lean shell ensures maximum flexibility for integrated micro-frontends and applications.

A standardized layout, defined by clear principles, is essential for:

  • Reducing Cognitive Load: Developers can navigate any frontend application and immediately understand where to find different types of logic, regardless of the underlying framework.
  • Promoting Best Practices: The structure should guide developers toward scalable, maintainable architectures that prioritize developer autonomy and efficiency.
  • Simplifying Tooling: Consistency in principles makes it easier to configure and apply tooling across diverse frontend projects.

Decision

We will adopt a standardized, framework-agnostic layout for all frontend applications, emphasizing a feature-sliced and domain-driven approach. This structure is designed to group code by logical domain or feature, making it easier to scale, maintain, and integrate diverse technologies. The core shell application will be a thin wrapper providing only essential identity and context management.

Core Principles for Frontend Layout

  1. Feature-Slicing: Organize code by feature or domain (e.g., authentication, user-management, product-catalog). All code related to a specific feature (components, services, state, routing) should be co-located within its dedicated directory.
  2. Framework Agnostic Structure: Avoid framework-specific naming conventions or directory structures at the top level. Instead, define logical sections like src/features, src/shared, src/core that can be adapted to any framework.
  3. Clear Boundaries: Establish clear distinctions between:
    • features/: Domain-specific logic and UI.
    • shared/: Truly generic, reusable UI components, utilities, and hooks that are purely presentational and framework-agnostic where possible.
    • core/: Application-wide concerns like authentication, routing, and global state management, implemented in a way that minimizes framework coupling.
  4. Minimalist Shell: The shell application will be a lightweight container responsible solely for:
    • Authentication (login/logout)
    • User switching
    • Tenant switching It will not include navigation menus or other application-specific UI elements, allowing integrated applications to define their own navigation and layout.

Conceptual Application Structure Example

/
└── src/
├── features/ # Domain-driven or feature-sliced modules
│ ├── authentication/ # e.g., login, registration, password reset
│ │ ├── components/
│ │ ├── services/
│ │ ├── hooks/ (or state management)
│ │ └── index.ts (or module entry)
│ └── user-profile/
│ ├── components/
│ ├── services/
│ └── ...
├── shared/ # Reusable, framework-agnostic components, utilities, types
│ ├── components/ # e.g., Button, Modal, Input
│ ├── utils/ # e.g., date formatters, validation helpers
│ ├── hooks/ (if applicable)
│ └── types/
├── core/ # Application-wide concerns (e.g., global state, routing setup)
│ ├── auth/ # Authentication logic, context
│ ├── routing/ # Main application routing configuration
│ └── config/ # Environment-specific configurations
├── assets/ # Static assets (images, fonts)
├── styles/ # Global styles, design tokens
└── main.ts (or index.js) # Application entry point

Consequences

Positive

  • Enhanced Developer Experience: Teams gain autonomy to select frameworks and tools best suited for their feature, leading to higher productivity and satisfaction.
  • Maximum Flexibility: The framework-agnostic approach and minimalist shell allow for seamless integration of diverse micro-frontends and future technology adoption.
  • Improved Scalability & Maintainability: Clear, domain-driven boundaries and feature-slicing promote independent development and deployment, reducing coupling.
  • Reduced Technical Debt: By not enforcing a single framework, we mitigate the risk of accumulating debt tied to a specific technology's lifecycle.

Negative

  • Increased Initial Setup for New Projects: While principles are shared, each new project might require slightly different boilerplate depending on the chosen framework.
  • Potential for Inconsistent UI/UX (Mitigation Required): Without a single framework, maintaining a consistent look and feel across different micro-frontends will require strong design system governance and shared component libraries.
  • Learning Curve for New Developers: Developers might need to be familiar with multiple frameworks if they work across different parts of the system.
  • Discipline Required: Teams must adhere strictly to the defined architectural principles to prevent fragmentation and ensure interoperability.

Decision

We will adopt a standardized, feature-sliced layout for all frontend applications within the apps/ directory. This structure is designed to group code by feature, making it easier to scale and maintain.

Angular Application Structure Example

/
└── src/
├── app/
│ ├── app.component.ts
│ ├── app.module.ts
│ └── app-routing.module.ts
├── assets/
│ ├── fonts/
│ └── images/
├── core/ # Core module with singleton services & guards
│ ├── guards/
│ │ └── auth.guard.ts
│ ├── interceptors/
│ │ └── jwt.interceptor.ts
│ ├── services/
│ │ └── api.service.ts
│ └── core.module.ts
├── features/ # Feature-sliced modules
│ ├── authentication/
│ │ ├── components/ # Components used only by this feature
│ │ │ └── login-form/
│ │ │ └── login-form.component.ts
│ │ ├── services/ # Services used only by this feature
│ │ ├── +state/ # State management (e.g., NgRx)
│ │ ├── authentication.module.ts
│ │ └── authentication-routing.module.ts
│ └── ...
├── pages/ # Top-level page components (routes)
│ ├── home/
│ │ └── home.component.ts
│ └── profile/
│ │ └── profile.component.ts
├── shared/ # Shared components, directives, pipes
│ ├── components/
│ │ └── button/
│ │ └── button.component.ts
│ ├── directives/
│ │ └── highlight.directive.ts
│ ├── pipes/
│ │ └── format-date.pipe.ts
│ └── shared.module.ts
├── styles/
│ └── styles.scss
├── environments/
│ ├── environment.ts
│ └── environment.prod.ts
└── main.ts # Application entry point

Directory Responsibilities

  1. features/:

    • Purpose: The most important directory. Each subdirectory is a lazy-loaded Angular NgModule representing a "vertical slice" of application functionality (e.g., authentication, user-profile, invoicing-list).
    • Contents: All the code related to a single feature is co-located here: its specific components, services, state management logic (e.g., NgRx), and routing. This is the core of the feature-sliced architecture.
  2. core/:

    • Purpose: Contains singleton services, route guards, and HTTP interceptors that should only be instantiated once in the application's lifetime.
    • Contents: AuthGuard, JwtInterceptor, a global ApiService. This module is imported only by the root AppModule.
  3. shared/:

    • Purpose: Contains truly generic, reusable UI components, directives, and pipes that are purely presentational. They are shared across multiple feature modules.
    • Contents: A SharedModule that exports ButtonComponent, CardComponent, FormatDatePipe, etc. This module can be safely imported by any feature module.
  4. pages/:

    • Purpose: Composes features and shared components into full pages that are mapped to application routes.
    • Contents: Components that represent a whole page, like DashboardPage or SettingsPage.
  5. environments/:

    • Purpose: Manages environment-specific configuration, like API endpoints.
    • Contents: environment.ts for development and environment.prod.ts for production builds.

Consequences

Positive

  • Scalability & Maintainability: As the application grows, new features can be added as self-contained modules without cluttering the global scope. Deleting a feature is as simple as deleting its folder.
  • Improved Developer Experience: Co-locating feature-specific code makes it much faster to find and modify related files.
  • Clear Boundaries: The structure creates a clear distinction between singleton services (core/), shared reusable code (shared/), and feature-specific code (features/).

Negative

  • Discipline Required: Developers must be disciplined about deciding what is truly a "shared" component versus a feature-specific one. Placing too much in shared/ can lead back to a less maintainable structure.
  • Initial Overhead: For a very small, simple application, this structure might feel like overkill. However, it is a necessary investment for the long-term health of a complex platform like Citadel.