Documentation
adrs/046-library-projects.md
ADR-046: Library Projects in Dynaplex
Status
Accepted
Updated: 2026-01-08 (db-manager consolidation)
Scope
This ADR defines runtime library projects only—pure, stateless code packages that are loaded into microservices and used at runtime.
Excluded from this ADR: Development tooling such as Dynaplex infrastructure, Roslyn analyzers, build scripts, project templates, and compatibility shims. These are governed by separate conventions and live in strata/ under the Acsis.Dynaplex.* namespace.
Context
The Dynaplex architecture currently distinguishes between three primary concepts:
- Components: Independent microservices following the 4-project pattern (
.Abstractions,.Database, service,.ApiClient) - Projects: Deployment compositions that combine components into deployable solutions
- Bases: Orchestration helpers that assist in composing components for deployment scenarios
However, some runtime code doesn't fit cleanly into the component model. This code is:
- Pure, stateless utilities (encoding, parsing, validation)
- Shared domain primitives (strongly-typed IDs, value objects)
- Runtime infrastructure abstractions (observability, resilience)
Currently, these are handled via an ad-hoc whitelist in AbstractionReferenceRequirementAnalyzer:
private static readonly string[] _allowedProjects = [
"Acsis.Dynaplex",
"Acsis.Dynaplex.RoslynAnalyzers",
"Acsis.Dynaplex.EntLibCompatShim",
"Acsis.Dynaplex.Strata.ServiceDefaults",
"Acsis.Dynaplex.Strata.Orchestration",
"Acsis.Encoding.Gs1"
];
This approach has several problems:
- No governance: Any project can be added to the whitelist without criteria
- Unclear intent: New developers don't understand why these projects are "special"
- Hidden architecture: The concept exists but isn't documented or formalized
- Conflated concerns: Development tooling and runtime libraries are mixed together
- No enforcement: Nothing prevents these "library" projects from accumulating inappropriate dependencies
The team has identified a need to share certain code directly (without HTTP overhead) while maintaining the strict boundaries that make Dynaplex valuable. The concern is that introducing a formal "library" concept could become an escape hatch that developers abuse to bypass component isolation.
Decision
We will introduce a formal Library project type as a first-class architectural concept in Dynaplex.
Definition
A Library is a pure, stateless code package that:
- Provides cross-cutting utilities or shared domain primitives
- Can be directly referenced by components, bases, and projects
- Does NOT run as a service, own persistence, or expose endpoints
- Is subject to strict dependency constraints enforced by Roslyn analyzers
Folder Structure and Naming
Runtime libraries live in a dedicated libraries/ root directory:
libraries/
├── encoding/
│ └── src/
│ └── Acsis.Dynaplex.Strata.Gs1/
│
├── domain-primitives/
│ └── src/
│ └── Acsis.Dynaplex.Strata.DomainPrimitives/
│
├── validation/
│ └── src/
│ └── Acsis.Dynaplex.Strata.Validation/
│
└── infrastructure/
└── src/
├── Acsis.Dynaplex.Strata.Observability/
└── Acsis.Dynaplex.Strata.Resilience/
Naming convention: Acsis.Dynaplex.Strata.{Name}
The Acsis.Dynaplex.Strata.* namespace prefix serves as a strong signal that a project:
- Is safe to reference as a runtime dependency
- Is pure, stateless, and side-effect-free
- Meets all library qualification criteria
Note: Development tooling (Dynaplex, analyzers, compatibility shims) uses the Acsis.Dynaplex.* namespace and lives in strata/.
Qualification Criteria
A project qualifies as a library when ALL of these are true:
| Criterion | Description |
|---|---|
| No persistence | Does not own a database schema, has no DbContext, no migrations |
| No endpoints | Has no Program.cs, no HTTP endpoints, no background services |
| Pure logic | Deterministic, side-effect-free (or near-pure with only logging) |
| Cross-cutting | Used by ≥2 components, or by components AND bases/projects |
| Stable semantics | Changes rarely; when it does, changes are coordinated globally |
| No business workflows | No domain policies, no orchestration, no service coordination |
The "workflow test": If you can describe the code as "business rule for [bounded context]" or "process that coordinates [services]", it belongs in a component, not a library.
What Libraries ARE (Examples)
| Category | Examples |
|---|---|
| Technical utilities | GS1/barcode encoding, parsing helpers, serialization utilities |
| Domain primitives | Strongly-typed IDs (PTIDs, PassportId), value objects, units of measure |
| Validation | Attribute-based validators, FluentValidation extensions, common rules |
| Runtime infrastructure | Observability helpers, resilience patterns, messaging abstractions |
What Libraries ARE NOT
| Anti-pattern | Why it's wrong | Where it belongs |
|---|---|---|
| Business workflows | Libraries can't orchestrate processes | Component service |
| Domain logic for one context | Not cross-cutting | Component's .Abstractions |
| Code that calls other services | Libraries must be pure | Component service |
| Code that owns data | Libraries can't have persistence | Component's .Database |
| HTTP endpoints | Libraries can't be deployed | Component service |
Dependency Rules (Enforced by Analyzers)
Libraries CANNOT reference:
| Forbidden | Rationale |
|---|---|
Acsis.Dynaplex.Engines.* |
Libraries must be independent of components |
*.Database projects |
Libraries cannot depend on persistence |
*.ApiClient projects |
Libraries cannot call other services |
Microsoft.EntityFrameworkCore.* |
Libraries cannot own data |
Microsoft.AspNetCore.* |
Libraries cannot expose endpoints |
Direct HttpClient usage |
Libraries should not make HTTP calls |
Libraries CAN reference:
| Allowed | Examples |
|---|---|
Other Acsis.Dynaplex.Strata.* |
Library can use shared primitives from another library |
| BCL/System namespaces | System.Text.Json, System.Collections.Generic, etc. |
| Logging abstractions | Microsoft.Extensions.Logging.Abstractions |
| Configuration abstractions | Microsoft.Extensions.Options |
| Pure utility packages | Humanizer, Polly (for retry logic types), etc. |
Reference Direction Rules
┌─────────────────────────────────────────────────────────────────┐
│ PROJECTS │
│ (Can reference everything: components, bases, libraries) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BASES │
│ (Can reference: components, libraries) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ COMPONENTS │
│ (Can reference: own projects, other .Abstractions, │
│ other .ApiClient, libraries) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LIBRARIES │
│ (Can reference: other libraries, BCL only) │
│ (CANNOT reference: components, bases, projects) │
└─────────────────────────────────────────────────────────────────┘
Analyzer Rules
New diagnostic rules enforce library constraints:
| Rule | Severity | Description |
|---|---|---|
| ACSIS0080 | Error | Library cannot reference component projects |
| ACSIS0081 | Error | Library cannot use Entity Framework Core |
| ACSIS0082 | Error | Library cannot use ASP.NET Core |
| ACSIS0083 | Warning | Library should not use HttpClient directly |
| ACSIS0084 | Error | Library cannot reference .Database projects |
| ACSIS0085 | Error | Library cannot reference .ApiClient projects |
The existing AbstractionReferenceRequirementAnalyzer will be updated to:
- Recognize
Acsis.Dynaplex.Strata.*as a valid reference target - Maintain backward compatibility with legacy whitelisted projects during migration
Governance
Creating a new library requires:
- Verification that ALL qualification criteria are met
- Architecture team review and approval
- Documentation of purpose in this ADR's appendix
PR checklist for library changes:
- Meets all qualification criteria
- No forbidden dependencies
- Unit tests for public API
- XML documentation complete
Periodic review (quarterly):
- Size audit: Flag libraries with >50 public types
- Dependency audit: Verify no inappropriate dependencies crept in
- Usage audit: Confirm library is still used by ≥2 consumers
Consequences
Positive
✅ Formalizes existing practice: The whitelisted projects already behave as libraries; this makes them official
✅ Clear governance: Explicit criteria prevent the library concept from becoming a dumping ground
✅ Analyzer enforcement: Compile-time errors prevent libraries from accumulating inappropriate dependencies
✅ Simplified mental model: "Library" is a well-understood concept in .NET; easier for new developers
✅ Honest architecture: Acknowledges that some code genuinely should be shared directly
✅ Reduced whitelist complexity: Instead of ad-hoc exceptions, a principled pattern
✅ Better discoverability: libraries/ folder makes shared code easy to find
Negative
⚠️ One more concept: Developers must understand Component vs Library distinction
⚠️ Analyzer complexity: Additional rules to maintain and test
⚠️ Migration effort: Existing projects need to be moved/renamed
⚠️ Governance overhead: Architecture review required for new libraries
⚠️ Potential abuse: Despite safeguards, developers may try to shortcut component creation
Risks and Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Developers put domain logic in libraries | Medium | High | Analyzer rules + "workflow test" + PR review |
| Libraries grow too large | Medium | Medium | Size audit + periodic review |
| Migration breaks builds | Low | High | Incremental migration, keep legacy whitelist |
| Concept adds confusion | Low | Medium | Clear documentation + examples |
Trade-offs Accepted
- Complexity vs Honesty: We accept slightly more architectural complexity in exchange for honestly representing how code is shared
- Governance vs Velocity: We accept slower library creation in exchange for maintaining architectural integrity
- Rules vs Flexibility: We accept strict analyzer rules in exchange for preventing architecture erosion
Implementation
Phase 1: Foundation
- Create this ADR
- Create
libraries/directory structure - Update ADR index
Phase 2: Analyzer Updates
- Update
AbstractionReferenceRequirementAnalyzerwith library detection - Create
LibraryConstraintAnalyzerwith ACSIS0080-0085 rules - Add comprehensive tests
Phase 3: Documentation
- Update
dynaplex-architecture.mdwith Library concept - Update
component-patterns.mdwith extraction guidance - Create
how-to/create-library.mdguide
Phase 4: Migration (Incremental)
- New libraries use
Acsis.Dynaplex.Strata.*naming immediately - Existing whitelisted projects migrated opportunistically
- Legacy whitelist maintained for backward compatibility
Appendix A: Project Classification
Runtime Libraries (This ADR)
These projects are or will be runtime libraries under Acsis.Dynaplex.Strata.*:
| Library | Purpose | Status |
|---|---|---|
Acsis.Encoding.Gs1 |
GS1 barcode encoding/decoding | External NuGet (to migrate to Acsis.Dynaplex.Strata.Gs1) |
Development Tooling (NOT Libraries)
These projects are development tooling under Acsis.Dynaplex.* in strata/:
| Project | Purpose | Location |
|---|---|---|
Acsis.Dynaplex |
Core infrastructure utilities, DbContext base, schema registry | strata/core/ |
Acsis.Dynaplex.Strata.ServiceDefaults |
Aspire service configuration defaults | strata/service-defaults/ |
Acsis.Dynaplex.Strata.Orchestration |
Component registration and orchestration helpers | strata/orchestration/ |
Acsis.Dynaplex.Strata.SourceGenerators |
Source generators for Dynaplex patterns | strata/source-generators/ |
Acsis.Dynaplex.RoslynAnalyzers |
Roslyn analyzers for architecture enforcement | strata/analyzers/ |
Acsis.Dynaplex.EntLibCompatShim |
Enterprise Library compatibility layer | strata/ent-lib-compat-shim/ |
Note: Development tooling is whitelisted in analyzers but is NOT a library—it uses the Acsis.Dynaplex.* namespace to clearly distinguish it from runtime code.
Appendix B: Decision Matrix
Use this matrix to determine if code belongs in a library or component:
START
│
▼
Does the code need to own data (database schema)?
│
├─ YES ──► COMPONENT (.Database project)
│
NO
│
▼
Does the code need to expose HTTP endpoints?
│
├─ YES ──► COMPONENT (service project)
│
NO
│
▼
Does the code implement business workflows or domain policies?
│
├─ YES ──► COMPONENT (.Abstractions + service)
│
NO
│
▼
Does the code call other services (HTTP, events)?
│
├─ YES ──► COMPONENT (service project)
│
NO
│
▼
Is the code used by only ONE component?
│
├─ YES ──► Keep in that component's .Abstractions
│
NO (used by 2+ components)
│
▼
Is the code pure/stateless (no I/O, no side effects)?
│
├─ NO ──► COMPONENT (reconsider design)
│
YES
│
▼
══════════════════════════
✓ LIBRARY CANDIDATE
══════════════════════════
Related Decisions
- ADR-001: Polylith Architecture Adoption (original inspiration; libraries are similar to Polylith components)
- ADR-004: Roslyn Analyzer Enforcement (mechanism for enforcing library constraints)
- ADR-007: Aspire Microservices Migration (established component model that libraries complement)
- ADR-033: Base Orchestration Pattern (introduced shared infrastructure concept)
- ADR-036: Database Project Separation (similar pattern of formalizing project types)
References
- Polylith Architecture - Original inspiration for component-based architecture
- DDD Shared Kernel - Pattern for sharing domain primitives
- Clean Architecture - Dependency rule inspiration