Documentation

reference/patterns/dynaplex-architecture.md

Dynaplex Architecture

Dynaplex is the architectural system powering Acsis Core - a modern microservices architecture that combines the modularity of Polylith with the power of .NET Aspire orchestration.

What is Dynaplex?

Dynaplex encompasses:

  • Multi-project component structure (.Abstractions, .Database, service, .ApiClient)
  • .NET Aspire orchestration for service management and discovery
  • Auto-generated API clients using Microsoft Kiota for type-safe communication
  • Database-per-service pattern with centralized migration management via db-manager
  • Strict architectural boundaries enforced by analyzers

Etymology: Dynamic + complex = Dynaplex - a system that's both flexible and sophisticated.

Core Principles

1. Microservices with Monorepo

All services live in one repository but run as independent processes:

engines/
├── core-data/          # Independent service
├── identity/           # Independent service
├── workflow/           # Independent service
└── [15 more services]

Benefits:

  • Atomic commits across services
  • Shared build infrastructure
  • Consistent development experience
  • Easy cross-service changes

2. Contract-First Development

Every component defines its contract in an .Abstractions project:

// Acsis.Dynaplex.Engines.CoreData.Abstractions/ICoreDataApi.cs
public interface ICoreDataApi
{
    Task<Asset?> GetAssetByIdAsync(int id);
    Task<Asset> CreateAssetAsync(CreateAssetRequest request);
}

Services can only reference other services' abstractions, never their implementations.

3. Service-to-Service Communication

At runtime, services communicate via HTTP using auto-generated clients:

// In Workflow service
public class WorkflowApi : IWorkflowApi
{
    private readonly ICoreDataApiClient _coreData;  // HTTP client

    public async Task ProcessAssetAsync(int assetId)
    {
        // HTTP call to CoreData service
        var asset = await _coreData.GetAssetByIdAsync(assetId);
    }
}

Why?

  • Services can scale independently
  • Failures are isolated
  • Technology mixing possible (see assettrak-ui Next.js component)

4. Aspire Orchestration

.NET Aspire manages the entire service ecosystem:

// projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure
var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("acsis");

// Services
var coreData = builder.AddProject<Acsis_Components_CoreData>("core-data")
    .WithReference(db);

var identity = builder.AddProject<Acsis_Components_Identity>("identity")
    .WithReference(db)
    .WithReference(coreData);  // Service dependency

builder.Build().Run();

Aspire automatically:

  • Manages service lifecycle
  • Configures service discovery
  • Sets up health checks
  • Provides telemetry
  • Handles configuration

Component Structure

Every Dynaplex component follows a four-project pattern:

engines/my-component/
├── src/
│   ├── Acsis.Dynaplex.Engines.MyComponent.Abstractions/
│   │   ├── IMyComponentApi.cs          # Contract
│   │   ├── Models/                     # DTOs
│   │   └── Configuration/              # Options
│   │
│   ├── Acsis.Dynaplex.Engines.MyComponent/
│   │   ├── Program.cs                  # ASP.NET Core entry point
│   │   ├── MyComponentApi.cs           # Implementation
│   │   └── Services/                   # Business logic
│   │
│   ├── Acsis.Dynaplex.Engines.MyComponent.Database/
│   │   ├── MyComponentDb.cs            # DbContext
│   │   ├── Entities/                   # Entity models
│   │   └── Migrations/                 # EF Core migrations
│   │
│   └── Acsis.Dynaplex.Engines.MyComponent.ApiClient/
│       └── [Generated by Kiota]        # HTTP client
│
├── test/
│   └── Acsis.Dynaplex.Engines.MyComponent.Tests/
│
└── resources/
    └── README.md                       # Component documentation

Why This Structure?

.Abstractions - Defines WHAT the service does

  • Contains interfaces and contracts
  • Can be referenced by other services
  • Enables compile-time type safety

.Database - Manages data model

  • Contains DbContext and entity models
  • Migrations stored here
  • Registered with centralized db-manager

Main Service - Implements HOW it's done

  • ASP.NET Core web service
  • Runs independently
  • Exposes HTTP API

.ApiClient - Provides type-safe client

  • Auto-generated from OpenAPI
  • Used by other services
  • Maintains type safety across HTTP

Note: Database migrations are run by the centralized db-manager component, not per-component migrators.

Library Structure

In addition to components, Dynaplex includes libraries—pure, stateless code packages that can be directly referenced without HTTP overhead.

What are Libraries?

Libraries provide cross-cutting utilities and shared primitives that don't fit the component (microservice) model:

  • Technical utilities: GS1 encoding, parsing, validation helpers
  • Domain primitives: Strongly-typed IDs (PTIDs), value objects, units of measure
  • Infrastructure abstractions: Dynaplex core utilities, ServiceDefaults

Unlike components, libraries:

  • Do NOT run as services
  • Do NOT own database schemas
  • Do NOT expose HTTP endpoints
  • CAN be directly referenced (no HTTP overhead)

Library vs Component

Aspect Library Component
Deployment Not deployed independently Independent microservice
Communication Direct project reference HTTP via ApiClient
Persistence Cannot own data Owns database schema
Structure Single project 4-project pattern
Purpose Pure utilities Business logic + APIs
Naming Acsis.Dynaplex.Strata.* Acsis.Dynaplex.Engines.*

Library Structure

libraries/
├── encoding/
│   └── src/Acsis.Dynaplex.Strata.Gs1/
│       └── [GS1 barcode encoding utilities]
│
├── domain-primitives/
│   └── src/Acsis.Dynaplex.Strata.DomainPrimitives/
│       └── [PTIDs, value objects, units]
│
├── validation/
│   └── src/Acsis.Dynaplex.Strata.Validation/
│       └── [Validation attributes and helpers]
│
└── infrastructure/
    └── src/
        ├── Acsis.Dynaplex.Strata.Dynaplex/
        ├── Acsis.Dynaplex.Strata.Dynaplex.ServiceDefaults/
        └── Acsis.Dynaplex.Strata.Dynaplex.Orchestration/

Qualification Criteria

A project qualifies as a library when ALL of these are true:

  1. No persistence: No DbContext, no migrations
  2. No endpoints: No Program.cs, no HTTP
  3. Pure logic: Deterministic, side-effect-free
  4. Cross-cutting: Used by ≥2 components
  5. Stable semantics: Changes rarely
  6. No business workflows: No domain policies or orchestration

The "workflow test": If you can describe the code as "business rule for [bounded context]", it belongs in a component, not a library.

Dependency Rules

Libraries are subject to strict constraints enforced by Roslyn analyzers:

Libraries CANNOT reference:

  • Acsis.Dynaplex.Engines.* (any component project)
  • .Database projects
  • .ApiClient projects
  • Entity Framework Core
  • ASP.NET Core

Libraries CAN reference:

  • Other Acsis.Dynaplex.Strata.* projects
  • BCL/System namespaces
  • Logging abstractions

See ADR-046: Library Projects for complete details.

Evolution from Polylith

Dynaplex evolved from a pure Polylith architecture:

Original (Polylith)

  • Dynamic component loading via AssemblyLoadContext
  • Single process, multiple components
  • Runtime composition

Current (Dynaplex)

  • Multiple independent services via Aspire
  • Microservices architecture
  • HTTP communication

Why the change?

See ADR-007: Evolution of Dynaplex

Key insights:

  • Better scalability with independent services
  • Cloud-native deployment with containers
  • Fault isolation between services
  • Easier to reason about in distributed systems

What stayed?

  • Four-project structure (evolved from Spec/Engine to Abstractions/Service)
  • Contract-first development
  • Strict architectural boundaries
  • Monorepo organization

Architectural Enforcement

Dynaplex enforces architectural rules through:

1. Roslyn Analyzers

ACSIS0001: Only .Abstractions projects may be referenced

// ❌ Compile error
using Acsis.Dynaplex.Engines.CoreData;

// ✅ Correct
using Acsis.Dynaplex.Engines.CoreData.Abstractions;

2. Physical Service Boundaries

Services run in separate processes - cannot directly reference implementations.

3. Generated Clients

Type-safe HTTP clients prevent runtime errors:

// Compile-time error if API changes
var asset = await _coreData.GetAssetByIdAsync(id);

Key Patterns

Database-Per-Service

Each service owns its database schema:

PostgreSQL
├── core_data schema      # Owned by CoreData service
├── identity schema       # Owned by Identity service
└── workflow schema       # Owned by Workflow service

Migrations run automatically before service startup.

Service Discovery

Aspire handles service discovery automatically:

// No manual configuration needed
var coreData = builder.AddProject<Acsis_Components_CoreData>("core-data");
var identity = builder.AddProject<Acsis_Components_Identity>("identity")
    .WithReference(coreData);  // Aspire configures URL automatically

Configuration

Services get configuration from multiple sources:

  1. appsettings.json - Defaults
  2. Environment variables - Deployment-specific
  3. Azure Key Vault - Secrets (in production)
  4. Aspire AppHost - Service URLs and references

Resilience

Built-in resilience patterns:

  • Retries - Automatic retry on transient failures
  • Circuit breakers - Prevent cascade failures
  • Timeouts - Fail fast on slow operations
  • Health checks - Monitor service health

Benefits of Dynaplex

Modularity - Clear component boundaries
Scalability - Scale services independently
Testability - Test services in isolation
Type Safety - Compile-time checks across services
Developer Experience - Aspire dashboard for local development
Cloud Native - Ready for containers and cloud platforms
Observability - Built-in telemetry and tracing
Consistency - Standardized patterns across all services

Trade-offs

Complexity - More moving parts than a monolith
Network overhead - HTTP calls between services
Distributed debugging - Harder to debug across services
Data consistency - No distributed transactions

Our stance: The benefits outweigh the trade-offs for our domain and scale.

Deployment Model

Local Development

dotnet run --project projects/bbu-rfid/src/Acsis.Dynaplex.Projects.BbuRfid/

Aspire starts:

  • All component services
  • PostgreSQL database
  • Development dashboard

Production (Azure Container Apps)

azd up

Aspire deploys:

  • Container per service
  • Managed PostgreSQL
  • Application Insights
  • Service-to-service networking

Key architectural decisions:

Further Reading


Dynaplex represents a pragmatic evolution from monolithic Polylith to cloud-native microservices while retaining the best aspects of component-based architecture.