Author: Mohammad Shakhtour

Status: Accepted

Context

The Problem

In enterprise applications with Clean Architecture and DDD, error handling presents significant challenges that impact both development velocity and user experience:

Challenge 1: Error Code Fragmentation

// Scattered across the codebase - no single source of truth
return Error.NotFound("ASSET_NOT_FOUND", "Asset not found");
return Error.NotFound("AssetNotFound", "Asset not found");
return Error.NotFound("Asset.NotFound", "Asset not found");

//OR

public const string AssetNotFoundErrorMessage = "Asset not found";
//etc..

Which format is correct? How does the frontend know what error codes exist?

Challenge 2: Frontend Localization

Modern applications require internationalization (i18n). The frontend needs:

Without a centralized error management system, this becomes manual and error-prone.

Challenge 3: Consistency Enforcement

How do we ensure:

Challenge 4: Discoverability

Why This Matters

For Frontend Teams:

> "We need a stable contract for error codes so we can implement proper localization and user-friendly error messages."

For Backend Developers:

> "We need clear guidance on where to define errors and confidence that our error codes won't conflict with others."

For System Reliability:

> "We need compile-time or test-time guarantees that our error handling is consistent and complete."

---

Decision Drivers

  1. Centralized Management - Single source of truth per aggregate/bounded context
  2. Frontend Integration - Export API for client-side localization (i18n/l10n)
  3. Discoverability - Easy to find all error codes via reflection
  4. Type Safety - Strongly-typed errors, not magic strings
  5. Architecture Governance - Automated test enforcement of patterns
  6. Layer Separation - Domain errors vs Application errors clearly distinguished
  7. Uniqueness Guarantee - Error codes must be unique, enforced by tests
  8. Developer Experience - Clear, consistent pattern that's hard to misuse

---

Decision

Implement an Attribute-Based Error Discovery System with centralized error definitions, with export, and architecture test enforcement.

Core Components

1. Attributes for Discovery

[ErrorCodeDefinition("Domain")] - Class-level attribute

[DomainError] - Field-level attribute

[ApplicationError] - Field-level attribute

2. Error Types by Layer

Domain Layer - DomainError

public sealed class DomainError
{
    public string Code { get; }
    public string Message { get; }
    
    public static DomainError Create(string code, string message)
        => new(code, message);
}

Application Layer - Error

public sealed class Error
{
    public string Code { get; }
    public string Message { get; }
    public ErrorType Type { get; }  // Validation, NotFound, Conflict, etc.
    
    public static Error NotFound(string code, string message) => ...;
    public static Error Validation(string code, string message) => ...;
    public static Error Conflict(string code, string message) => ...;
}

3. Exception Types

DomainException - Thrown from Domain Layer

public class DomainException : Exception
{
    public DomainError Error { get; }
    
    public DomainException(DomainError error) : base(error.Message)
    {
        Error = error;
    }
}

ApplicationException - Thrown from Application Layer

public class ApplicationException : Exception
{
    public Error Error { get; }
    
    public ApplicationException(Error error) : base(error.Message)
    {
        Error = error;
    }
}

Usage Philosophy:

4. Export System

ErrorExporter

public static class ErrorExporter
{
    public static ErrorExportResult ExportAll()
    {
        return new ErrorExportResult
        {
            DomainErrors = ExportDomainErrors(),
            ApplicationErrors = ExportApplicationErrors(),
            Timestamp = DateTime.UtcNow
        };
    }
}

Process:

  1. Scan domain assembly for classes with [ErrorCodeDefinition]
  2. Find fields with [DomainError] or [ApplicationError]
  3. Extract error code, message, type, domain, class name
  4. Return structured JSON for frontend consumption
  5. can be cached if needed

Export API Endpoints:

GET /api/v1/errors/export          # All errors
GET /api/v1/errors/application     # Application errors only
GET /api/v1/errors/domain          # Domain errors only

5. Architecture Test Enforcement

Tests Automatically Enforce:

[Fact]
public void DomainErrorClasses_ShouldHaveErrorCodeDefinitionAttribute()
{
    // All classes ending with "Errors" must have [ErrorCodeDefinition]
}

[Fact]
public void DomainErrorFields_ShouldHaveDomainErrorAttribute()
{
    // All DomainError fields must have [DomainError]
}

[Fact]
public void ApplicationErrorFields_ShouldHaveApplicationErrorAttribute()
{
    // All Error fields must have [ApplicationError]
}

[Fact]
public void AllErrorCodes_ShouldBeUnique()
{
    // Error codes must be unique within each layer
}

---

Implementation Patterns

Domain Layer Pattern

Domain Error Definition:

using CleanArchitecture.Cmms.Domain.Abstractions;
using CleanArchitecture.Cmms.Domain.Abstractions.Attributes;

namespace CleanArchitecture.Cmms.Domain.Assets;

[ErrorCodeDefinition("Asset")]
internal static class AssetErrors
{
    [DomainError]
    public static readonly DomainError AlreadyUnderMaintenance = DomainError.Create(
        "Asset.AlreadyUnderMaintenance",
        "Asset already under maintenance.");

    [DomainError]
    public static readonly DomainError NotUnderMaintenance = DomainError.Create(
        "Asset.NotUnderMaintenance",
        "Asset is not under maintenance.");

    [DomainError]
    public static readonly DomainError TagRequired = DomainError.Create(
        "AssetTag.TagRequired",
        "Asset tag cannot be empty.");
}

Usage in Domain:

public sealed class Asset : AggregateRoot<Guid>
{
    public Result SetUnderMaintenance()
    {
        if (_status == AssetStatus.UnderMaintenance)
        {
            // Invariant violation - throw exception
            throw new DomainException(AssetErrors.AlreadyUnderMaintenance);
        }
        
        _status = AssetStatus.UnderMaintenance;
        return Result.Success();
    }
}

Alternatives Considered

Option 1: Magic Strings Everywhere

Pattern:

return Error.NotFound("ASSET_NOT_FOUND", "Asset not found");
return Error.NotFound("AssetNotFound", "Asset not found");
return Error.NotFound("Asset.NotFound", "Asset not found");

Analysis:

Problems:

Verdict: Rejected - No discoverability, no consistency

---

Option 2: Enum-Based Error Codes

Pattern:

public enum ErrorCode
{
    AssetNotFound,
    AssetNotAvailable,
    WorkOrderNotFound,
    TechnicianNotAvailable
}

return Error.NotFound(ErrorCode.AssetNotFound, "Asset not found");

Pros:

Cons:

Verdict: Rejected - Violates bounded contexts, no message coupling

---

Option 3: Exception-Based Control Flow

Pattern:

public class AssetNotFoundException : DomainException { }
public class AssetNotAvailableException : DomainException { }

Analysis:

That's consider accepted and to have specific domain exception but still you need to define error code and message and also will require too many exceptions classes

Problems:

Verdict: Rejected

---

Implementation Guidelines

When to Use Domain vs Application Errors

Use DomainError when:

Use Application Error when:

Error Code Naming Convention

Format: {Aggregate}.{ErrorName}

Examples:

For nested types: {Aggregate}{Type}.{ErrorName}

---

Future Considerations

Potential Enhancements

1. Parametrized error

Pass values at runtime for the error template