Skip to content

Application Architecture

@azure-net/kit implements a hybrid architecture that combines principles of Domain Driven Design (DDD) and Clean Architecture. This allows for creating loosely coupled, testable, and easily extensible systems.

General Architecture Principles

1. Layer Separation

The architecture is built on four main layers:

  • Domain - business logic and domain models
  • Application - application services and orchestration
  • Infrastructure - external dependencies and adapters
  • Delivery - providing data to UI elements

2. Dependency Inversion

Inner layers do not depend on outer layers. Dependencies are directed inward toward the domain.

3. Context Separation

The system is divided into Bounded Contexts, each with its own domain area.

Example Context Division

src/
├── app/
│   ├── contexts/          # Bounded Contexts
│   │   ├── admin/         # Administrative context
│   │   └── account/       # Client context
│   ├── core/              # Base components for package setup and operation (used in contexts as core)
│   └── shared/            # shared layer - common and reusable elements that don't belong to a specific module or context

Detailed Context Structure

Each context has the following structure:

Domain Layer (contexts/{context}/domain/)

Purpose: Contains business logic and domain models.

domain/
├── {aggregate}/          # Domain aggregate
│   ├── model/            # Data models
│   │   └── index.ts      # Entity interfaces
│   ├── enums/            # Enumerations
│   ├── constants/        # Constants related to business entity
│   ├── ports/            # Ports (interfaces)
│   │   └── index.ts      # Contracts for external dependencies
│   └── index.ts          # Aggregate export

What's contained in Domain:

Model (Data models):

  • Domain entity interfaces
  • Example: IUser, IProduct

Enums (Enumerations):

  • Enums related to the entity
  • Example: user statuses, user roles

Ports (Ports):

  • Interfaces for external dependencies
  • Define contracts for repositories and services
  • Include data structures for input/output
  • Serve as a unified description of what exactly the domain expects from the external world and what it provides to the outside, while maintaining domain logic isolation.
  • Example: IUserRepository, IUserCreateRequest, IUserCollectionResponse, IUserCollectionQuery

Infrastructure Layer (contexts/{context}/infrastructure/)

Purpose: Implements technical details and external integrations.

infrastructure/
├── http/                 
│   └── repositories/     # Repository implementations
│       ├── UserRepository.ts
│       ├── AuthRepository.ts
│       └── index.ts
├── providers/            # Infrastructure providers
│   ├── InfrastructureProvider.ts
│   └── index.ts
└── index.ts

What's contained in Infrastructure:

Repositories (Repositories):

  • Implement ports from Domain layer
  • Encapsulate data access logic
  • Transform external data into domain models
  • Work with HTTP API

Providers (Providers):

  • Provide access to infrastructure layer components for other layers
  • Create and configure repositories and other components
  • Dependency injection (DataSources, HTTP clients)

Application Layer (contexts/{context}/application/)

Purpose: Working with scenarios that implement business logic at the application level.

application/
├── services/             # Application services
│   ├── JobRequestService.ts
│   ├── AuthService.ts
│   └── index.ts
├── providers/            # Application providers
│   ├── ApplicationProvider.ts
│   └── index.ts
└── index.ts

What's contained in Application:

Services (Services):

  • Implement repository methods
  • Create use cases and domain business logic implementation
  • Orchestrate calls to domain and infrastructure
  • Use ports from Domain layer
  • Example: user creation scenario, user update scenario, etc.

Providers (Providers):

  • Provide access to application layer components for other layers
  • Create and configure services
  • Dependency injection from infrastructure layer

Delivery Layer (contexts/{context}/delivery/)

Purpose: Handles user actions and provides data from the application layer.

delivery/
├── {feature}/            # Functional area
│   ├── {Feature}Presenter.ts  # Presenter
│   ├── schema/           # Validation schemas
│   │   ├── CreateSchema.ts
│   │   ├── UpdateSchema.ts
│   │   └── index.ts
│   └── consts/           # Constants
└── auth/                 # Authorization

What's contained in Delivery:

Presenters (Presenters):

  • Connect UI with Application layer
  • Handle user actions
  • Manage UI state

Schema (UI validation schemas):

  • Validate incoming data
  • Transform data for API
  • Ensure type safety

Everything required for UI (ui-constants, stores for states, etc.)

Core Components (app/core/)

Purpose: Base components for package setup and operation (used in contexts as core).

core/
├── datasources/          # Base data sources
├── providers/            # Base providers
├── presenters/           # Base presenters  
├── middleware/           # Components for setup and connection of universal middlewares to the project
├── responses/            # Base response handlers
└── schemas/              # Base UI validation schema settings

What's contained in Core:

DataSources:

  • HTTP clients for API interaction
  • Connection configuration
  • Error handling and retry logic

Providers:

  • Global dependency providers
  • DataSource creation and configuration

Presenters:

  • Base classes for presenters and their injection

etc.

Shared Components (app/shared/)

Purpose: common and reusable elements that don't belong to a specific module or context

shared/
├── helpers/              # Global helper functions
├── stores/               # Global state stores
├── types/                # Common types
└── etc.../               # Other folders required in shared for your project

Architecture Working Principles

1. Dependency Injection through Providers

The system uses the Provider Pattern for dependency management:

typescript
// Creating a provider with dependencies
export const ApplicationProvider = createBoundaryProvider('{ContextName}ApplicationProvider', {
  dependsOn: { InfrastructureProvider },
  register: ({ InfrastructureProvider }) => ({
    // Service registration
    UserService: () => new UserService(InfrastructureProvider.UserRepository),
    SystemHealthService: () => new SystemHealthService()
  }),
  boot: ({SystemHealthService}) => {
    // Allows access to already registered services and performs initialization, starts background processes or sets up observers.
    SystemHealthService.startMonitoring();
  }
});

2. Ports as Contracts

Ports define interfaces that must be implemented in external layers:

typescript
// Port in Domain layer
export interface IUserCreateRequest {
    name: string;
}

export interface IUserCreateResponse {
    token: string;
}

export interface IUserRepository {
  create(request: IUserCreateRequest): Promise<IUserCreateResponse>;
}

// Implementation in Infrastructure layer
export class UserRepository implements IUserRepository {
    
    create(request: IUserCreateRequest): Promise<IUserCreateResponse> {
        // Concrete implementation
    }
}

3. Presenters as UI Coordinators

Presenters connect the user interface with business logic:

typescript
export const UserPresenter = AppPresenter('{ContextName}UserPresenter', ({ createAsyncAction }) => {
  const { UserService } = ApplicationProvider();
  
  const create = async (data: IUserCreateRequest) => {
    return await createAsyncAction(() => UserService.create(data));
  };
  
  return { create };
});

Development Recommendations

When adding a new feature:

  1. Define a model in the Domain layer
  2. Create a port for external dependencies
  3. Create a repository in the Infrastructure layer
  4. Implement a service in the Application layer
  5. Update providers to bind dependencies
  6. Configure a presenter in the Delivery layer

Advantages of This Architecture

  1. Testability - each layer can be tested independently
  2. Extensibility - easy to add new features without changing existing code
  3. Clarity - clear separation of responsibilities
  4. Scalability - support for multiple contexts

This architecture ensures clear separation of responsibilities and allows creating reliable, testable, and easily maintainable applications.