Table of Contents

Domain vs Application

In the Neos framework, business code is organized into two distinct layers: Domain and Application. Understanding when to use each layer is essential for building maintainable and testable applications.

Understanding the layers

The Neos framework follows Clean Architecture principles where each layer has specific responsibilities:

Layer Purpose Contains
Domain Core business logic Entity validation rules, entity event rules, entity repositories
Application Application-specific logic Entity view validation rules, entity view event rules, server methods, notifications, data objects
graph TB
    subgraph Application["Application Layer"]
        EV[Entity Views]
        EVVR[Entity View Validation Rules]
        EVER[Entity View Event Rules]
        SM[Server Methods]
        NOT[Notifications]
        COMM[Inter-cluster Communication]
    end
    
    subgraph Domain["Domain Layer"]
        E[Entities]
        EVR[Entity Validation Rules]
        EER[Entity Event Rules]
        REPO[Repositories]
    end
    
    Application --> Domain
    Domain -.-x|Cannot reference| Application
    
    style Domain fill:#e8f5e9,color:#000
    style Application fill:#e3f2fd,color:#000
Important

The Domain layer must never reference the Application layer. This ensures that your core business logic remains independent of application-specific concerns.

Where to place your business code?

Prioritize the Domain layer

It is important to prioritize the Domain layer: this creates a "vault" of rules that cannot be bypassed. All entity views derived from an entity will trigger the rules defined on that entity. If a rule is placed at the entity level (Domain), it will always be executed regardless of the entry point.

However, if you place a rule only at the entity view level (Application), it will apply exclusively to that specific view. Creating a second entity view would then bypass this rule entirely. To guarantee that a rule is enforced regardless of the entry point, place it at the entity level (Domain).

flowchart TB
    subgraph Domain["Domain Layer"]
        RULES[Entity<br/>Validation & Event Rules]
    end
    
    subgraph Application["Application Layer"]
        EV1[Entity View 1<br/>Validation & Event Rules]
        EV2[Entity View 2<br/>Validation & Event Rules]
    end
    
    EV1 --> RULES
    EV2 --> RULES
    
    style Domain fill:#e8f5e9,color:#000
    style Application fill:#e3f2fd,color:#000
Tip

Place business rules that must always be enforced (regardless of which entity view is used) in the Domain layer (entity validation rules and entity event rules).

When to use the Application layer

On the other hand, as soon as you need to use elements specific to the Application layer, you will have no choice but to position your code in the Application layer:

  • Server methods: Exposing operations via API endpoints
  • Notifications: Sending real-time notifications to clients
  • Entity view-specific logic: Rules that should only apply to specific entity views
  • Inter-cluster communication: Calling APIs from other clusters

Nothing prevents you from creating an entry point in the Application layer which calls helpers defined in the Domain layer.

Decision flow

flowchart TD
    START[New Business Logic] --> Q1{Does it use server methods,<br/>notifications, or anything defined in the application layer?}
    Q1 -->|Yes| APP[Place in Application Layer]
    Q1 -->|No| Q2{Must it execute for<br/>ALL entity views?}
    Q2 -->|Yes| DOM[Place in Domain Layer]
    Q2 -->|No| Q3{Is it entity view specific?}
    Q3 -->|Yes| APP
    Q3 -->|No| DOM
    
    style DOM fill:#e8f5e9,color:#000
    style APP fill:#e3f2fd,color:#000
Tip

Rule of thumb: If your code represents a fundamental business rule that must always be enforced regardless of how the data is accessed, place it in the Domain layer. If it's specific to a particular use case or requires application services, use the Application layer.

Entity rules in the Application layer

In some cases, you may need to define entity validation rules or entity event rules (which are always executed regardless of which entity view is used) that depend on Application layer services (server methods, notifications, data objects, etc.).

Neos allows you to attach rules to an entity (Domain concept) while placing the implementation in the Application layer. This gives you:

  • Guaranteed execution: The rule is triggered for all entity views of the entity, making it "inviolable"
  • Access to Application services: The rule can use notifications, call server methods...

In Neos Studio, when creating an entity validation rule or entity event rule, check the "Show Application layer assemblies" option to select an Application layer business assembly.

Note

Use this pattern when you have a critical business rule that must always be enforced but requires Application layer dependencies. For example, sending a notification whenever an order is saved, regardless of which entity view triggered the save.

Helpers

Creating helpers allows you to share code and create utility methods available from the business code. Helpers can be placed in either layer depending on their dependencies.

Domain layer helper example

A helper in the Domain layer can only use Domain layer services (repositories, entities):

// Interface in Domain layer
public interface IDashboardHelper
{
    Task<NeosWidget?> GetWidgetAsync(int id, CancellationToken cancellationToken = default);
    Task<NeosDashboardInformation?> GetDashboardAsync(int id, CancellationToken cancellationToken = default);
    Task SetSharedDashboardAsync(int id, bool isShared, CancellationToken cancellationToken = default);
}

// Implementation in Domain layer
public class DashboardHelper : IDashboardHelper
{
    private readonly INeosDashboardInformationRepository _dashboardInformationRepository;
    private readonly INeosWidgetRepository _widgetRepository;

    public DashboardHelper(
        INeosDashboardInformationRepository dashboardInformationRepository,
        INeosWidgetRepository widgetRepository)
    {
        _dashboardInformationRepository = dashboardInformationRepository;
        _widgetRepository = widgetRepository;
    }

    public Task<NeosDashboardInformation?> GetDashboardAsync(int id, CancellationToken cancellationToken = default)
    {
        return _dashboardInformationRepository.FindAsync(id, cancellationToken);
    }

    public Task<NeosWidget?> GetWidgetAsync(int id, CancellationToken cancellationToken = default)
    {
        return _widgetRepository.FindAsync(id, cancellationToken);
    }

    public async Task SetSharedDashboardAsync(int id, bool isShared, CancellationToken cancellationToken = default)
    {
        NeosDashboardInformation? dashboard = await GetDashboardAsync(id, cancellationToken);
        if (dashboard is not null)
        {
            dashboard.IsShared = isShared;
        }
    }
}

Application layer helper example

A helper in the Application layer can use any service, including those from the Domain layer:

// Interface in Application layer
public interface INeosTenantInfoHelper
{
    void PopulateAdditionalProperties(IEnumerable<NeosTenantInfo> collection);
}

// Implementation in Application layer
public class NeosTenantInfoHelper : INeosTenantInfoHelper
{
    private readonly IAdditionalPropertyRepository _additionalPropertyRepository;
    private readonly IAdditionalPropertyMetadataRepository _additionalPropertyMetadataRepository;

    public NeosTenantInfoHelper(
        IAdditionalPropertyRepository additionalPropertyRepository,
        IAdditionalPropertyMetadataRepository additionalPropertyMetadataRepository)
    {
        _additionalPropertyRepository = additionalPropertyRepository;
        _additionalPropertyMetadataRepository = additionalPropertyMetadataRepository;
    }

    public void PopulateAdditionalProperties(IEnumerable<NeosTenantInfo> collection)
    {
        // Implementation that works with data objects and repositories
    }
}

Cross-layer pattern

You can create an entry point in the Application layer that delegates to Domain layer helpers:

// Application layer service calling Domain helper
public class OrderApplicationService
{
    private readonly IOrderDomainHelper _domainHelper; // Domain layer helper
    private readonly INotificationContext _notificationContext; // Application layer service

    public OrderApplicationService(
        IOrderDomainHelper domainHelper,
        INotificationContext notificationContext)
    {
        _domainHelper = domainHelper;
        _notificationContext = notificationContext;
    }

    public async Task ProcessOrderAsync(int orderId)
    {
        // Use Domain helper for core business logic
        OrderResult result = _domainHelper.ValidateAndProcessOrder(orderId);
        
        // Use Application layer service for notifications
        await _notificationContext.SendToCurrentConnectionAsync(
            "OrderProcessed",
            new { OrderId = orderId, Status = result.Status });
    }
}

With or without interface

If there is no abstraction, then the implementation will be tested where it is used. This has the following advantages. For example, if you have a saving rule on an order header with shared processing to control the header information, the unit tests must include all the data required for the complete implementation to function correctly.

This ensures that the unit tests include all the necessary business logic and are closer to reality:

  • When there is systematic abstraction, the helpers are mocked up, but if there is refactoring, the unit tests are very much impacted.
  • When all the services/helpers are mocked up, the high-level unit test often does not reflect reality because there is no TDD or in any case too many mocks.
  • There's nothing to stop you doing unit tests for the implementation of the shared helper.
Tip

Recommendation: Use interfaces when:

  • The helper has complex dependencies that are hard to set up in tests
  • The helper performs I/O operations (database, file system, network)
  • You need different implementations (e.g., for testing or different environments)

Skip interfaces when:

  • The helper is a simple utility with no external dependencies
  • The logic is straightforward and easily testable inline

Registering helpers

To use your helpers via dependency injection, you need to register them. See how to register additional services for detailed instructions.

See also