Claude Skill

dotnet-architecture

Design or review .NET solution architecture across modular monoliths, clean architecture, vertical slices, microservices, DDD, CQRS, and cloud-native boundaries without over-engineering.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-architecture-bfa4ebd.zip · 13 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

Install

skills CLI npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-architecture
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

.NET Architecture

Trigger On

  • choosing architecture for a new or evolving .NET system
  • reviewing layer boundaries, domain boundaries, or service decomposition
  • deciding whether clean architecture, vertical slices, CQRS, or microservices are justified

Workflow

  1. Start from business capability boundaries and change frequency, not from a preferred diagram style.
  2. Use simple modular monolith patterns by default, and move to microservices only when team autonomy, scale, or deployment boundaries justify the added operational cost.
  3. Apply DDD and CQRS where business rules are genuinely complex; avoid forcing aggregates and command pipelines into CRUD-heavy code with no payoff.
  4. Keep dependencies flowing inward when using clean architecture, but avoid creating extra projects that add ceremony without ownership clarity.
  5. Make integration boundaries explicit: contracts, storage ownership, messaging, consistency model, and observability expectations.
  6. Use dotnet-aspire when local orchestration, service discovery, and developer observability are part of the architecture story.

Deliver

  • an architecture direction that matches system complexity
  • clear project and dependency boundaries
  • migration notes or tradeoffs when changing an existing structure

Validate

  • the proposed structure reduces rather than increases accidental complexity
  • data ownership and integration paths are explicit
  • the architecture is testable and operable, not just diagram-friendly

References

  • references/patterns.md - detailed implementations of Clean Architecture, Vertical Slices, DDD, CQRS, Modular Monolith, and Microservices with C# 12+ examples
  • references/anti-patterns.md - common architectural mistakes including over-abstraction, anemic domain models, premature microservices, and cargo cult patterns
Files (dotnet-skills)
  • references
    • anti-patterns.md 22 KB
      # Architectural Anti-Patterns in .NET
      
      This reference documents common architectural mistakes in .NET systems and how to avoid them. Examples use C# 12+ features including primary constructors.
      
      ---
      
      ## Over-Abstraction
      
      ### Problem: Unnecessary Interfaces
      
      Creating interfaces for every class, even when there is only one implementation and no testing or extension need.
      
      ```csharp
      // Anti-pattern: Interface for the sake of interface
      public interface IOrderService
      {
          Task<Order> GetOrderAsync(Guid id);
      }
      
      public class OrderService(AppDbContext db) : IOrderService
      {
          public async Task<Order> GetOrderAsync(Guid id)
              => await db.Orders.FindAsync(id);
      }
      
      // Then injected as:
      services.AddScoped<IOrderService, OrderService>();
      ```
      
      ### Solution
      
      Use interfaces when there is a real need: multiple implementations, testing seams, or abstraction over infrastructure.
      
      ```csharp
      // Better: Direct class when no abstraction is needed
      public sealed class OrderService(AppDbContext db)
      {
          public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct = default)
              => await db.Orders.FindAsync([id], ct);
      }
      
      // Register directly:
      services.AddScoped<OrderService>();
      
      // Use interface when there is a reason:
      // - Repository abstracts persistence (testable, swappable)
      // - External service client abstracts HTTP (mockable)
      // - Strategy pattern requires polymorphism
      ```
      
      ---
      
      ## Anemic Domain Model
      
      ### Problem: Logic in Services, Data in Entities
      
      Entities become data bags while business logic lives in service classes, losing the benefits of encapsulation.
      
      ```csharp
      // Anti-pattern: Anemic entity
      public class Order
      {
          public Guid Id { get; set; }
          public Guid CustomerId { get; set; }
          public List<OrderLine> Lines { get; set; } = [];
          public string Status { get; set; } = "Draft";
          public decimal Total { get; set; }
      }
      
      // Logic scattered in service
      public class OrderService(AppDbContext db)
      {
          public async Task SubmitOrderAsync(Guid orderId)
          {
              var order = await db.Orders.Include(o => o.Lines).FirstAsync(o => o.Id == orderId);
      
              if (order.Lines.Count == 0)
                  throw new Exception("Cannot submit empty order");
      
              if (order.Status != "Draft")
                  throw new Exception("Order already submitted");
      
              order.Status = "Submitted";
              order.Total = order.Lines.Sum(l => l.Quantity * l.UnitPrice);
      
              await db.SaveChangesAsync();
          }
      }
      ```
      
      ### Solution
      
      Encapsulate behavior in the entity. Keep invariants protected.
      
      ```csharp
      // Better: Rich domain model
      public sealed class Order
      {
          public OrderId Id { get; }
          public CustomerId CustomerId { get; }
          private readonly List<OrderLine> _lines = [];
          public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
          public OrderStatus Status { get; private set; }
          public Money Total => _lines.Aggregate(Money.Zero, (sum, l) => sum + l.Subtotal);
      
          private Order(OrderId id, CustomerId customerId)
          {
              Id = id;
              CustomerId = customerId;
              Status = OrderStatus.Draft;
          }
      
          public static Order Create(CustomerId customerId)
              => new(OrderId.New(), customerId);
      
          public void Submit()
          {
              if (_lines.Count == 0)
                  throw new DomainException("Cannot submit an empty order.");
      
              if (Status != OrderStatus.Draft)
                  throw new DomainException("Only draft orders can be submitted.");
      
              Status = OrderStatus.Submitted;
          }
      }
      ```
      
      ---
      
      ## Big Ball of Mud
      
      ### Problem: No Clear Boundaries
      
      Everything depends on everything. Controllers call repositories directly. Business logic lives in controllers. Database models leak into API responses.
      
      ```csharp
      // Anti-pattern: Controller doing everything
      [ApiController]
      public class OrderController(AppDbContext db, IEmailService email) : ControllerBase
      {
          [HttpPost]
          public async Task<IActionResult> CreateOrder(OrderDto dto)
          {
              // Validation mixed with persistence
              if (dto.Lines.Count == 0)
                  return BadRequest("Order must have lines");
      
              var customer = await db.Customers.FindAsync(dto.CustomerId);
              if (customer == null)
                  return NotFound("Customer not found");
      
              // Business logic in controller
              var order = new Order
              {
                  Id = Guid.NewGuid(),
                  CustomerId = dto.CustomerId,
                  Lines = dto.Lines.Select(l => new OrderLine
                  {
                      ProductId = l.ProductId,
                      Quantity = l.Quantity,
                      UnitPrice = l.UnitPrice
                  }).ToList(),
                  Status = "Created",
                  Total = dto.Lines.Sum(l => l.Quantity * l.UnitPrice)
              };
      
              // Infrastructure concern in controller
              db.Orders.Add(order);
              await db.SaveChangesAsync();
      
              // Side effect mixed in
              await email.SendOrderConfirmationAsync(customer.Email, order.Id);
      
              // Database entity leaked to response
              return Ok(order);
          }
      }
      ```
      
      ### Solution
      
      Separate concerns into layers or slices with clear responsibilities.
      
      ```csharp
      // Better: Clear separation
      [ApiController]
      public class OrderController(IMediator mediator) : ControllerBase
      {
          [HttpPost]
          public async Task<IActionResult> CreateOrder(CreateOrderRequest request, CancellationToken ct)
          {
              var result = await mediator.Send(new CreateOrderCommand(request), ct);
      
              return result.Match(
                  success: id => CreatedAtAction(nameof(GetOrder), new { id }, new { id }),
                  failure: errors => BadRequest(errors));
          }
      }
      
      // Handler owns the use case
      public sealed class CreateOrderHandler(
          IOrderRepository orders,
          ICustomerRepository customers,
          IEventPublisher events) : IRequestHandler<CreateOrderCommand, Result<Guid>>
      {
          public async Task<Result<Guid>> Handle(CreateOrderCommand command, CancellationToken ct)
          {
              var customer = await customers.GetByIdAsync(command.CustomerId, ct);
              if (customer is null)
                  return Result<Guid>.NotFound("Customer not found.");
      
              var order = Order.Create(customer.Id);
      
              foreach (var line in command.Lines)
                  order.AddLine(line.ProductId, line.Quantity, line.UnitPrice);
      
              await orders.AddAsync(order, ct);
              await orders.SaveChangesAsync(ct);
      
              await events.PublishAsync(new OrderCreatedEvent(order.Id, customer.Id), ct);
      
              return Result<Guid>.Success(order.Id.Value);
          }
      }
      ```
      
      ---
      
      ## Premature Microservices
      
      ### Problem: Distributed Monolith
      
      Splitting into microservices before understanding domain boundaries. Services are tightly coupled, require synchronous calls, and share databases.
      
      ```csharp
      // Anti-pattern: Synchronous cross-service calls in critical path
      public sealed class OrderService(
          HttpClient customerClient,
          HttpClient inventoryClient,
          HttpClient pricingClient)
      {
          public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
          {
              // Synchronous dependency on customer service
              var customer = await customerClient.GetFromJsonAsync<CustomerDto>(
                  $"/customers/{request.CustomerId}", ct);
      
              if (customer is null)
                  throw new Exception("Customer not found");
      
              // Synchronous dependency on inventory
              foreach (var item in request.Items)
              {
                  var available = await inventoryClient.GetFromJsonAsync<bool>(
                      $"/inventory/{item.ProductId}/available?quantity={item.Quantity}", ct);
      
                  if (!available)
                      throw new Exception($"Product {item.ProductId} not available");
              }
      
              // Synchronous dependency on pricing
              var prices = await pricingClient.PostAsJsonAsync("/pricing/calculate", request.Items, ct);
      
              // If any service is down, order creation fails completely
              // ...
          }
      }
      ```
      
      ### Solution
      
      Start with a modular monolith. Extract services only when there is a clear ownership, scale, or deployment boundary need.
      
      ```csharp
      // Better: Modular monolith with clear contracts
      public sealed class OrderService(
          ISalesModule sales,
          IInventoryModule inventory,
          IEventBus events)
      {
          public async Task<Result<Guid>> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
          {
              // In-process call to customer module
              var customer = await sales.GetCustomerAsync(request.CustomerId, ct);
              if (customer is null)
                  return Result<Guid>.NotFound("Customer not found.");
      
              // Reserve inventory (can be made async with events if needed)
              var reservation = await inventory.ReserveAsync(request.Items, ct);
              if (!reservation.IsSuccess)
                  return Result<Guid>.Failure("Inventory unavailable.");
      
              var order = Order.Create(customer.Id, reservation.Items);
              await sales.SaveOrderAsync(order, ct);
      
              // Async event for downstream processing
              await events.PublishAsync(new OrderCreatedEvent(order.Id), ct);
      
              return Result<Guid>.Success(order.Id.Value);
          }
      }
      
      // Module boundary is explicit and can be extracted later
      public interface IInventoryModule
      {
          Task<ReservationResult> ReserveAsync(IEnumerable<OrderItem> items, CancellationToken ct);
          Task ReleaseReservationAsync(Guid reservationId, CancellationToken ct);
      }
      ```
      
      ---
      
      ## Repository Explosion
      
      ### Problem: One Repository Per Entity
      
      Creating a repository for every entity, leading to dozens of nearly identical classes with slight variations.
      
      ```csharp
      // Anti-pattern: Repository per entity
      public interface IOrderRepository
      {
          Task<Order?> GetByIdAsync(Guid id);
          Task AddAsync(Order order);
          Task UpdateAsync(Order order);
          Task DeleteAsync(Guid id);
      }
      
      public interface IOrderLineRepository
      {
          Task<OrderLine?> GetByIdAsync(Guid id);
          Task AddAsync(OrderLine line);
          // ... same methods
      }
      
      public interface ICustomerRepository { /* same pattern */ }
      public interface IProductRepository { /* same pattern */ }
      // ... 30 more repositories
      ```
      
      ### Solution
      
      Repository per aggregate root, not per entity. Use the DbContext directly for simple queries.
      
      ```csharp
      // Better: Repository per aggregate
      public interface IOrderRepository
      {
          Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
          Task<Order?> GetWithLinesAsync(OrderId id, CancellationToken ct = default);
          Task AddAsync(Order order, CancellationToken ct = default);
          Task SaveChangesAsync(CancellationToken ct = default);
      }
      
      // Order is the aggregate root; OrderLine is part of the aggregate
      // No separate OrderLineRepository needed
      
      // For simple read queries, use DbContext directly or a query service
      public sealed class OrderQueryService(AppDbContext db)
      {
          public async Task<List<OrderSummaryDto>> GetRecentOrdersAsync(
              CustomerId customerId,
              int count = 10,
              CancellationToken ct = default)
          {
              return await db.Orders
                  .Where(o => o.CustomerId == customerId)
                  .OrderByDescending(o => o.CreatedAt)
                  .Take(count)
                  .Select(o => new OrderSummaryDto(o.Id, o.Status, o.Total, o.CreatedAt))
                  .ToListAsync(ct);
          }
      }
      ```
      
      ---
      
      ## God Object / God Service
      
      ### Problem: One Class Does Everything
      
      A single service class grows to handle all operations for a domain area, becoming unmaintainable.
      
      ```csharp
      // Anti-pattern: God service
      public class OrderService(
          AppDbContext db,
          IEmailService email,
          IPaymentGateway payments,
          IInventoryService inventory,
          IShippingService shipping,
          IPricingEngine pricing,
          IDiscountCalculator discounts,
          ITaxCalculator taxes,
          ILogger<OrderService> logger)
      {
          public async Task<Order> CreateOrderAsync(...) { /* 200 lines */ }
          public async Task SubmitOrderAsync(...) { /* 150 lines */ }
          public async Task CancelOrderAsync(...) { /* 100 lines */ }
          public async Task RefundOrderAsync(...) { /* 180 lines */ }
          public async Task UpdateShippingAsync(...) { /* 80 lines */ }
          public async Task ApplyDiscountAsync(...) { /* 90 lines */ }
          public async Task RecalculateTotalsAsync(...) { /* 120 lines */ }
          // ... 20 more methods, 3000+ lines total
      }
      ```
      
      ### Solution
      
      Split by use case or subdomain. Use vertical slices or focused handlers.
      
      ```csharp
      // Better: Focused handlers per use case
      public sealed class CreateOrderHandler(
          IOrderRepository orders,
          ICustomerRepository customers) : IRequestHandler<CreateOrderCommand, Result<Guid>>
      {
          public async Task<Result<Guid>> Handle(CreateOrderCommand command, CancellationToken ct)
          {
              // Focused, testable, ~50 lines
          }
      }
      
      public sealed class SubmitOrderHandler(
          IOrderRepository orders,
          IPaymentService payments,
          IEventPublisher events) : IRequestHandler<SubmitOrderCommand, Result>
      {
          public async Task<Result> Handle(SubmitOrderCommand command, CancellationToken ct)
          {
              // Focused, testable, ~60 lines
          }
      }
      
      public sealed class CancelOrderHandler(
          IOrderRepository orders,
          IRefundService refunds,
          IInventoryService inventory) : IRequestHandler<CancelOrderCommand, Result>
      {
          public async Task<Result> Handle(CancelOrderCommand command, CancellationToken ct)
          {
              // Focused, testable, ~70 lines
          }
      }
      ```
      
      ---
      
      ## Leaky Abstractions
      
      ### Problem: Infrastructure Concerns Leak Upward
      
      Database implementation details, HTTP concerns, or framework specifics appear in business logic.
      
      ```csharp
      // Anti-pattern: EF Core specifics in domain
      public class Order
      {
          // EF navigation property concerns in domain entity
          public virtual Customer Customer { get; set; }
          public virtual ICollection<OrderLine> Lines { get; set; }
      
          // EF change tracking awareness
          public void AddLine(OrderLine line)
          {
              Lines ??= new List<OrderLine>();
              Lines.Add(line);
          }
      }
      
      // Anti-pattern: HTTP concerns in application layer
      public sealed class OrderHandler(HttpClient http)
      {
          public async Task<Order> GetOrderAsync(Guid id)
          {
              var response = await http.GetAsync($"/api/orders/{id}");
      
              // HTTP status codes in business logic
              if (response.StatusCode == HttpStatusCode.NotFound)
                  throw new OrderNotFoundException(id);
      
              if (response.StatusCode == HttpStatusCode.Forbidden)
                  throw new UnauthorizedException();
      
              // JSON parsing in handler
              var json = await response.Content.ReadAsStringAsync();
              return JsonSerializer.Deserialize<Order>(json);
          }
      }
      ```
      
      ### Solution
      
      Keep infrastructure concerns in infrastructure layer. Use domain-appropriate abstractions.
      
      ```csharp
      // Better: Clean domain entity
      public sealed class Order
      {
          public OrderId Id { get; }
          private readonly List<OrderLine> _lines = [];
          public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
      
          public void AddLine(ProductId productId, Quantity quantity, Money unitPrice)
          {
              _lines.Add(new OrderLine(productId, quantity, unitPrice));
          }
      }
      
      // Better: Infrastructure handles HTTP, exposes domain result
      public sealed class OrderServiceClient(HttpClient http) : IOrderServiceClient
      {
          public async Task<Result<Order>> GetOrderAsync(OrderId id, CancellationToken ct = default)
          {
              try
              {
                  var response = await http.GetAsync($"/api/orders/{id.Value}", ct);
      
                  if (response.StatusCode == HttpStatusCode.NotFound)
                      return Result<Order>.NotFound();
      
                  response.EnsureSuccessStatusCode();
      
                  var dto = await response.Content.ReadFromJsonAsync<OrderDto>(ct);
                  return Result<Order>.Success(dto!.ToDomain());
              }
              catch (HttpRequestException)
              {
                  return Result<Order>.Failure("Order service unavailable.");
              }
          }
      }
      
      // Application layer uses domain abstractions
      public sealed class OrderQueryHandler(IOrderServiceClient client)
      {
          public async Task<Result<OrderDto>> Handle(GetOrderQuery query, CancellationToken ct)
          {
              var result = await client.GetOrderAsync(query.OrderId, ct);
              return result.Map(order => OrderDto.FromDomain(order));
          }
      }
      ```
      
      ---
      
      ## Cargo Cult Architecture
      
      ### Problem: Copying Patterns Without Understanding
      
      Implementing patterns because "that's how it's done" without understanding the problem they solve.
      
      ```csharp
      // Anti-pattern: CQRS + Event Sourcing for a simple CRUD app
      public sealed class UpdateCustomerEmailHandler(
          IEventStore eventStore,
          IEventBus eventBus,
          IReadModelProjector projector)
      {
          public async Task Handle(UpdateCustomerEmailCommand command, CancellationToken ct)
          {
              // Load entire event history
              var events = await eventStore.GetEventsAsync(command.CustomerId, ct);
              var customer = Customer.FromHistory(events);
      
              // Apply change
              customer.UpdateEmail(command.NewEmail);
      
              // Save event
              var newEvent = new CustomerEmailUpdatedEvent(command.CustomerId, command.NewEmail);
              await eventStore.AppendAsync(command.CustomerId, newEvent, ct);
      
              // Publish for projections
              await eventBus.PublishAsync(newEvent, ct);
      
              // Update read model
              await projector.ProjectAsync(newEvent, ct);
          }
      }
      
      // All this for: UPDATE Customers SET Email = @Email WHERE Id = @Id
      ```
      
      ### Solution
      
      Match architecture complexity to problem complexity. Simple problems deserve simple solutions.
      
      ```csharp
      // Better: Simple solution for simple problem
      public sealed class UpdateCustomerEmailHandler(
          AppDbContext db,
          IValidator<UpdateCustomerEmailCommand> validator) : IRequestHandler<UpdateCustomerEmailCommand, Result>
      {
          public async Task<Result> Handle(UpdateCustomerEmailCommand command, CancellationToken ct)
          {
              var validation = await validator.ValidateAsync(command, ct);
              if (!validation.IsValid)
                  return Result.Invalid(validation.Errors);
      
              var customer = await db.Customers.FindAsync([command.CustomerId], ct);
              if (customer is null)
                  return Result.NotFound();
      
              customer.Email = command.NewEmail;
              await db.SaveChangesAsync(ct);
      
              return Result.Success();
          }
      }
      
      // Use CQRS/Event Sourcing when you actually need:
      // - Audit trail of all changes
      // - Temporal queries (state at point in time)
      // - Complex event-driven workflows
      // - Different read/write scaling requirements
      ```
      
      ---
      
      ## Missing Error Handling Strategy
      
      ### Problem: Inconsistent Error Handling
      
      Errors handled differently across the codebase. Some throw exceptions, some return null, some return result objects.
      
      ```csharp
      // Anti-pattern: Inconsistent error handling
      public class OrderService
      {
          public async Task<Order> GetOrderAsync(Guid id)
          {
              var order = await db.Orders.FindAsync(id);
              return order; // Returns null, caller must check
          }
      
          public async Task SubmitOrderAsync(Guid id)
          {
              var order = await db.Orders.FindAsync(id);
              if (order == null)
                  throw new NotFoundException("Order not found"); // Throws exception
          }
      
          public async Task<bool> CancelOrderAsync(Guid id)
          {
              var order = await db.Orders.FindAsync(id);
              if (order == null)
                  return false; // Returns bool
      
              order.Cancel();
              await db.SaveChangesAsync();
              return true;
          }
      }
      ```
      
      ### Solution
      
      Adopt a consistent result pattern across the application.
      
      ```csharp
      // Better: Consistent result type
      public readonly struct Result<T>
      {
          public T? Value { get; }
          public ResultError? Error { get; }
          public bool IsSuccess => Error is null;
      
          private Result(T value) => Value = value;
          private Result(ResultError error) => Error = error;
      
          public static Result<T> Success(T value) => new(value);
          public static Result<T> Failure(string message) => new(new ResultError(message));
          public static Result<T> NotFound(string? message = null) => new(new ResultError(message ?? "Not found", ResultErrorType.NotFound));
      
          public TResult Match<TResult>(Func<T, TResult> success, Func<ResultError, TResult> failure)
              => IsSuccess ? success(Value!) : failure(Error!);
      }
      
      // Consistent usage across handlers
      public sealed class GetOrderHandler(IOrderRepository orders) : IRequestHandler<GetOrderQuery, Result<OrderDto>>
      {
          public async Task<Result<OrderDto>> Handle(GetOrderQuery query, CancellationToken ct)
          {
              var order = await orders.GetByIdAsync(query.OrderId, ct);
              if (order is null)
                  return Result<OrderDto>.NotFound();
      
              return Result<OrderDto>.Success(OrderDto.FromDomain(order));
          }
      }
      
      public sealed class SubmitOrderHandler(IOrderRepository orders) : IRequestHandler<SubmitOrderCommand, Result>
      {
          public async Task<Result> Handle(SubmitOrderCommand command, CancellationToken ct)
          {
              var order = await orders.GetByIdAsync(command.OrderId, ct);
              if (order is null)
                  return Result.NotFound();
      
              var submitResult = order.Submit();
              if (!submitResult.IsSuccess)
                  return submitResult;
      
              await orders.SaveChangesAsync(ct);
              return Result.Success();
          }
      }
      ```
      
      ---
      
      ## Summary: Pattern Selection Guidance
      
      | Smell | Question to Ask | If Yes | If No |
      |-------|-----------------|--------|-------|
      | Adding interface for every class | Is there more than one implementation or a testing need? | Keep interface | Remove interface |
      | Logic in services, data in entities | Are there business invariants to protect? | Move logic to entity | Service is fine for orchestration |
      | Considering microservices | Do teams need independent deployment? Different scale needs? | Consider microservices | Stay with modular monolith |
      | Adding event sourcing | Need audit trail, temporal queries, or complex workflows? | Event sourcing may help | Simple persistence is fine |
      | Repository per entity | Is this entity an aggregate root? | Repository is appropriate | Access through aggregate root |
      | God service growing | Does this class have multiple reasons to change? | Split by use case | Keep if cohesive |
      
      ---
      
      ## References
      
      - [Anemic Domain Model by Martin Fowler](https://martinfowler.com/bliki/AnemicDomainModel.html)
      - [Big Ball of Mud by Brian Foote and Joseph Yoder](http://www.laputan.org/mud/)
      - [Monolith First by Martin Fowler](https://martinfowler.com/bliki/MonolithFirst.html)
      - [Don't Start with Microservices by Sam Newman](https://samnewman.io/blog/2015/04/07/microservices-for-greenfield/)
      
    • patterns.md 21.1 KB
      # Architectural Patterns for .NET
      
      This reference covers practical implementations of common architectural patterns in modern .NET, using C# 12+ features including primary constructors.
      
      ---
      
      ## Clean Architecture
      
      Clean Architecture organizes code into concentric layers with dependencies pointing inward. The domain layer has no external dependencies; infrastructure concerns live at the outer edge.
      
      ### Layer Structure
      
      ```text
      src/
        Domain/           # Entities, value objects, domain events, interfaces
        Application/      # Use cases, DTOs, validators, command/query handlers
        Infrastructure/   # EF Core, external APIs, messaging, file storage
        WebApi/           # Controllers, middleware, composition root
      ```
      
      ### Domain Layer
      
      ```csharp
      // Domain/Entities/Order.cs
      public sealed class Order
      {
          public OrderId Id { get; }
          public CustomerId CustomerId { get; }
          private readonly List<OrderLine> _lines = [];
          public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
          public OrderStatus Status { get; private set; }
          public Money Total => _lines.Aggregate(Money.Zero, (sum, line) => sum + line.Subtotal);
      
          private Order(OrderId id, CustomerId customerId)
          {
              Id = id;
              CustomerId = customerId;
              Status = OrderStatus.Draft;
          }
      
          public static Order Create(CustomerId customerId)
              => new(OrderId.New(), customerId);
      
          public void AddLine(Product product, Quantity quantity)
          {
              if (Status != OrderStatus.Draft)
                  throw new DomainException("Cannot modify a submitted order.");
      
              var existing = _lines.Find(l => l.ProductId == product.Id);
              if (existing is not null)
                  existing.IncreaseQuantity(quantity);
              else
                  _lines.Add(new OrderLine(product.Id, product.Price, quantity));
          }
      
          public void Submit()
          {
              if (_lines.Count == 0)
                  throw new DomainException("Cannot submit an empty order.");
      
              Status = OrderStatus.Submitted;
          }
      }
      
      // Domain/ValueObjects/Money.cs
      public readonly record struct Money(decimal Amount, string Currency)
      {
          public static Money Zero => new(0, "USD");
      
          public static Money operator +(Money a, Money b)
          {
              if (a.Currency != b.Currency)
                  throw new InvalidOperationException("Currency mismatch.");
              return new Money(a.Amount + b.Amount, a.Currency);
          }
      }
      
      // Domain/Interfaces/IOrderRepository.cs
      public interface IOrderRepository
      {
          Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
          Task AddAsync(Order order, CancellationToken ct = default);
          Task SaveChangesAsync(CancellationToken ct = default);
      }
      ```
      
      ### Application Layer
      
      ```csharp
      // Application/Orders/Commands/SubmitOrder/SubmitOrderCommand.cs
      public sealed record SubmitOrderCommand(Guid OrderId) : IRequest<Result>;
      
      // Application/Orders/Commands/SubmitOrder/SubmitOrderHandler.cs
      public sealed class SubmitOrderHandler(
          IOrderRepository orders,
          IEventPublisher events) : IRequestHandler<SubmitOrderCommand, Result>
      {
          public async Task<Result> Handle(SubmitOrderCommand command, CancellationToken ct)
          {
              var order = await orders.GetByIdAsync(new OrderId(command.OrderId), ct);
              if (order is null)
                  return Result.NotFound("Order not found.");
      
              order.Submit();
              await orders.SaveChangesAsync(ct);
              await events.PublishAsync(new OrderSubmittedEvent(order.Id, order.Total), ct);
      
              return Result.Success();
          }
      }
      ```
      
      ### Infrastructure Layer
      
      ```csharp
      // Infrastructure/Persistence/OrderRepository.cs
      public sealed class OrderRepository(AppDbContext db) : IOrderRepository
      {
          public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default)
              => await db.Orders
                  .Include(o => o.Lines)
                  .FirstOrDefaultAsync(o => o.Id == id, ct);
      
          public async Task AddAsync(Order order, CancellationToken ct = default)
              => await db.Orders.AddAsync(order, ct);
      
          public Task SaveChangesAsync(CancellationToken ct = default)
              => db.SaveChangesAsync(ct);
      }
      ```
      
      ---
      
      ## Vertical Slice Architecture
      
      Vertical slices organize code by feature instead of by layer. Each slice owns its request, handler, validator, and data access. This reduces cross-cutting changes and keeps related code together.
      
      ### Folder Structure
      
      ```text
      src/
        Features/
          Orders/
            CreateOrder/
              CreateOrderEndpoint.cs
              CreateOrderHandler.cs
              CreateOrderRequest.cs
              CreateOrderValidator.cs
            GetOrder/
              GetOrderEndpoint.cs
              GetOrderHandler.cs
          Products/
            ...
        Shared/
          Infrastructure/
          Extensions/
      ```
      
      ### Feature Implementation
      
      ```csharp
      // Features/Orders/CreateOrder/CreateOrderRequest.cs
      public sealed record CreateOrderRequest(
          Guid CustomerId,
          List<OrderLineDto> Lines);
      
      public sealed record OrderLineDto(Guid ProductId, int Quantity);
      
      // Features/Orders/CreateOrder/CreateOrderValidator.cs
      public sealed class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
      {
          public CreateOrderValidator()
          {
              RuleFor(x => x.CustomerId).NotEmpty();
              RuleFor(x => x.Lines).NotEmpty();
              RuleForEach(x => x.Lines).ChildRules(line =>
              {
                  line.RuleFor(l => l.ProductId).NotEmpty();
                  line.RuleFor(l => l.Quantity).GreaterThan(0);
              });
          }
      }
      
      // Features/Orders/CreateOrder/CreateOrderHandler.cs
      public sealed class CreateOrderHandler(
          AppDbContext db,
          IValidator<CreateOrderRequest> validator) : IRequestHandler<CreateOrderRequest, Result<Guid>>
      {
          public async Task<Result<Guid>> Handle(CreateOrderRequest request, CancellationToken ct)
          {
              var validation = await validator.ValidateAsync(request, ct);
              if (!validation.IsValid)
                  return Result<Guid>.Invalid(validation.Errors);
      
              var order = Order.Create(new CustomerId(request.CustomerId));
      
              foreach (var line in request.Lines)
              {
                  var product = await db.Products.FindAsync([line.ProductId], ct);
                  if (product is null)
                      return Result<Guid>.NotFound($"Product {line.ProductId} not found.");
      
                  order.AddLine(product, new Quantity(line.Quantity));
              }
      
              await db.Orders.AddAsync(order, ct);
              await db.SaveChangesAsync(ct);
      
              return Result<Guid>.Success(order.Id.Value);
          }
      }
      
      // Features/Orders/CreateOrder/CreateOrderEndpoint.cs
      public static class CreateOrderEndpoint
      {
          public static void Map(IEndpointRouteBuilder app) =>
              app.MapPost("/orders", async (
                  CreateOrderRequest request,
                  IMediator mediator,
                  CancellationToken ct) =>
              {
                  var result = await mediator.Send(request, ct);
                  return result.Match(
                      success: id => Results.Created($"/orders/{id}", new { id }),
                      failure: errors => Results.BadRequest(errors));
              })
              .WithName("CreateOrder")
              .WithTags("Orders")
              .Produces<Guid>(StatusCodes.Status201Created)
              .ProducesValidationProblem();
      }
      ```
      
      ---
      
      ## Domain-Driven Design (DDD)
      
      DDD applies when business rules are complex and need explicit modeling. Avoid forcing DDD into simple CRUD scenarios.
      
      ### Aggregate Root
      
      ```csharp
      // Domain/Aggregates/Booking/Booking.cs
      public sealed class Booking : AggregateRoot<BookingId>
      {
          public RoomId RoomId { get; }
          public GuestId GuestId { get; }
          public DateRange Period { get; private set; }
          public BookingStatus Status { get; private set; }
      
          private Booking(BookingId id, RoomId roomId, GuestId guestId, DateRange period)
              : base(id)
          {
              RoomId = roomId;
              GuestId = guestId;
              Period = period;
              Status = BookingStatus.Pending;
          }
      
          public static Booking Create(RoomId roomId, GuestId guestId, DateRange period)
          {
              var booking = new Booking(BookingId.New(), roomId, guestId, period);
              booking.AddDomainEvent(new BookingCreatedEvent(booking.Id, roomId, period));
              return booking;
          }
      
          public Result Confirm()
          {
              if (Status != BookingStatus.Pending)
                  return Result.Failure("Only pending bookings can be confirmed.");
      
              Status = BookingStatus.Confirmed;
              AddDomainEvent(new BookingConfirmedEvent(Id));
              return Result.Success();
          }
      
          public Result Cancel(string reason)
          {
              if (Status == BookingStatus.Cancelled)
                  return Result.Failure("Booking is already cancelled.");
      
              if (Status == BookingStatus.Confirmed && Period.Start <= DateTime.UtcNow.AddDays(1))
                  return Result.Failure("Cannot cancel confirmed booking within 24 hours.");
      
              Status = BookingStatus.Cancelled;
              AddDomainEvent(new BookingCancelledEvent(Id, reason));
              return Result.Success();
          }
      }
      
      // Domain/ValueObjects/DateRange.cs
      public readonly record struct DateRange(DateTime Start, DateTime End)
      {
          public int Nights => (End - Start).Days;
      
          public bool Overlaps(DateRange other)
              => Start < other.End && End > other.Start;
      
          public static DateRange Create(DateTime start, DateTime end)
          {
              if (end <= start)
                  throw new DomainException("End date must be after start date.");
              return new DateRange(start, end);
          }
      }
      ```
      
      ### Domain Events
      
      ```csharp
      // Domain/Events/BookingConfirmedEvent.cs
      public sealed record BookingConfirmedEvent(BookingId BookingId) : IDomainEvent;
      
      // Application/Handlers/BookingConfirmedHandler.cs
      public sealed class BookingConfirmedHandler(
          IEmailService email,
          IBookingRepository bookings) : IDomainEventHandler<BookingConfirmedEvent>
      {
          public async Task Handle(BookingConfirmedEvent @event, CancellationToken ct)
          {
              var booking = await bookings.GetByIdAsync(@event.BookingId, ct);
              if (booking is null) return;
      
              await email.SendConfirmationAsync(booking.GuestId, booking.Period, ct);
          }
      }
      ```
      
      ### Domain Service
      
      ```csharp
      // Domain/Services/BookingPolicyService.cs
      public sealed class BookingPolicyService(IRoomRepository rooms)
      {
          public async Task<Result> CanBookAsync(
              RoomId roomId,
              DateRange period,
              CancellationToken ct = default)
          {
              var room = await rooms.GetByIdAsync(roomId, ct);
              if (room is null)
                  return Result.Failure("Room not found.");
      
              if (!room.IsAvailable)
                  return Result.Failure("Room is not available for booking.");
      
              var conflicts = await rooms.GetOverlappingBookingsAsync(roomId, period, ct);
              if (conflicts.Count > 0)
                  return Result.Failure("Room is already booked for the requested period.");
      
              if (period.Nights > room.MaxStayNights)
                  return Result.Failure($"Maximum stay is {room.MaxStayNights} nights.");
      
              return Result.Success();
          }
      }
      ```
      
      ---
      
      ## CQRS (Command Query Responsibility Segregation)
      
      CQRS separates write operations (commands) from read operations (queries). Apply it when read and write models have different optimization needs.
      
      ### Command Side
      
      ```csharp
      // Application/Commands/PlaceOrder/PlaceOrderCommand.cs
      public sealed record PlaceOrderCommand(
          Guid CustomerId,
          List<OrderItemDto> Items) : ICommand<Result<Guid>>;
      
      // Application/Commands/PlaceOrder/PlaceOrderHandler.cs
      public sealed class PlaceOrderHandler(
          IOrderRepository orders,
          IUnitOfWork unitOfWork,
          IEventBus events) : ICommandHandler<PlaceOrderCommand, Result<Guid>>
      {
          public async Task<Result<Guid>> Handle(PlaceOrderCommand command, CancellationToken ct)
          {
              var order = Order.Create(new CustomerId(command.CustomerId));
      
              foreach (var item in command.Items)
              {
                  order.AddItem(new ProductId(item.ProductId), item.Quantity, item.UnitPrice);
              }
      
              await orders.AddAsync(order, ct);
              await unitOfWork.CommitAsync(ct);
      
              await events.PublishAsync(new OrderPlacedIntegrationEvent(
                  order.Id.Value,
                  order.CustomerId.Value,
                  order.Total.Amount), ct);
      
              return Result<Guid>.Success(order.Id.Value);
          }
      }
      ```
      
      ### Query Side with Dedicated Read Model
      
      ```csharp
      // Application/Queries/GetOrderSummary/GetOrderSummaryQuery.cs
      public sealed record GetOrderSummaryQuery(Guid OrderId) : IQuery<OrderSummaryDto?>;
      
      // Application/Queries/GetOrderSummary/OrderSummaryDto.cs
      public sealed record OrderSummaryDto(
          Guid Id,
          string CustomerName,
          DateTime OrderDate,
          string Status,
          decimal Total,
          List<OrderItemSummaryDto> Items);
      
      public sealed record OrderItemSummaryDto(
          string ProductName,
          int Quantity,
          decimal UnitPrice,
          decimal Subtotal);
      
      // Application/Queries/GetOrderSummary/GetOrderSummaryHandler.cs
      public sealed class GetOrderSummaryHandler(
          IDbConnection db) : IQueryHandler<GetOrderSummaryQuery, OrderSummaryDto?>
      {
          public async Task<OrderSummaryDto?> Handle(GetOrderSummaryQuery query, CancellationToken ct)
          {
              const string sql = """
                  SELECT o.Id, c.Name AS CustomerName, o.OrderDate, o.Status, o.Total
                  FROM Orders o
                  JOIN Customers c ON o.CustomerId = c.Id
                  WHERE o.Id = @OrderId;
      
                  SELECT oi.ProductName, oi.Quantity, oi.UnitPrice, oi.Subtotal
                  FROM OrderItems oi
                  WHERE oi.OrderId = @OrderId;
                  """;
      
              await using var multi = await db.QueryMultipleAsync(sql, new { query.OrderId });
      
              var order = await multi.ReadSingleOrDefaultAsync<OrderSummaryDto>();
              if (order is null) return null;
      
              var items = (await multi.ReadAsync<OrderItemSummaryDto>()).ToList();
              return order with { Items = items };
          }
      }
      ```
      
      ### Event Sourcing Integration
      
      ```csharp
      // Infrastructure/EventStore/OrderEventStore.cs
      public sealed class OrderEventStore(EventStoreClient client) : IOrderEventStore
      {
          public async Task AppendAsync(OrderId id, IEnumerable<IDomainEvent> events, CancellationToken ct)
          {
              var streamName = $"order-{id.Value}";
              var eventData = events.Select(e => new EventData(
                  Uuid.NewUuid(),
                  e.GetType().Name,
                  JsonSerializer.SerializeToUtf8Bytes(e),
                  null)).ToArray();
      
              await client.AppendToStreamAsync(streamName, StreamState.Any, eventData, cancellationToken: ct);
          }
      
          public async Task<Order> RehydrateAsync(OrderId id, CancellationToken ct)
          {
              var streamName = $"order-{id.Value}";
              var events = new List<IDomainEvent>();
      
              await foreach (var resolved in client.ReadStreamAsync(
                  Direction.Forwards, streamName, StreamPosition.Start, cancellationToken: ct))
              {
                  var eventType = Type.GetType($"Domain.Events.{resolved.Event.EventType}");
                  var domainEvent = (IDomainEvent)JsonSerializer.Deserialize(
                      resolved.Event.Data.Span, eventType!)!;
                  events.Add(domainEvent);
              }
      
              return Order.FromHistory(id, events);
          }
      }
      ```
      
      ---
      
      ## Modular Monolith
      
      A modular monolith keeps code in one deployable unit while enforcing module boundaries. Modules communicate through defined contracts, enabling future extraction.
      
      ### Module Structure
      
      ```text
      src/
        Modules/
          Sales/
            Sales.Api/
            Sales.Application/
            Sales.Domain/
            Sales.Infrastructure/
            Sales.Contracts/          # Public contracts other modules can depend on
          Inventory/
            Inventory.Api/
            Inventory.Application/
            Inventory.Domain/
            Inventory.Infrastructure/
            Inventory.Contracts/
        Host/                         # Composition root, startup, shared infrastructure
      ```
      
      ### Module Contracts
      
      ```csharp
      // Modules/Sales/Sales.Contracts/Events/OrderPlacedIntegrationEvent.cs
      public sealed record OrderPlacedIntegrationEvent(
          Guid OrderId,
          Guid CustomerId,
          List<OrderedProduct> Products,
          DateTime OccurredAt) : IIntegrationEvent;
      
      public sealed record OrderedProduct(Guid ProductId, int Quantity);
      
      // Modules/Sales/Sales.Contracts/Services/ISalesModule.cs
      public interface ISalesModule
      {
          Task<OrderSummaryDto?> GetOrderSummaryAsync(Guid orderId, CancellationToken ct = default);
      }
      ```
      
      ### Inter-Module Communication
      
      ```csharp
      // Modules/Inventory/Inventory.Application/Handlers/OrderPlacedHandler.cs
      public sealed class OrderPlacedHandler(
          IInventoryRepository inventory,
          IUnitOfWork unitOfWork) : IIntegrationEventHandler<OrderPlacedIntegrationEvent>
      {
          public async Task Handle(OrderPlacedIntegrationEvent @event, CancellationToken ct)
          {
              foreach (var product in @event.Products)
              {
                  var item = await inventory.GetByProductIdAsync(product.ProductId, ct);
                  if (item is null) continue;
      
                  item.Reserve(product.Quantity);
              }
      
              await unitOfWork.CommitAsync(ct);
          }
      }
      
      // Host/Program.cs - Module registration
      builder.Services
          .AddSalesModule(builder.Configuration)
          .AddInventoryModule(builder.Configuration)
          .AddSharedInfrastructure(builder.Configuration);
      ```
      
      ---
      
      ## Microservices Boundaries
      
      When microservices are justified, define clear ownership and communication patterns.
      
      ### Service Contract
      
      ```csharp
      // Contracts/OrderService/IOrderServiceClient.cs
      public interface IOrderServiceClient
      {
          Task<OrderDto?> GetOrderAsync(Guid orderId, CancellationToken ct = default);
          Task<Guid> PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct = default);
      }
      
      // Infrastructure/Clients/OrderServiceClient.cs
      public sealed class OrderServiceClient(
          HttpClient http,
          ILogger<OrderServiceClient> logger) : IOrderServiceClient
      {
          public async Task<OrderDto?> GetOrderAsync(Guid orderId, CancellationToken ct = default)
          {
              try
              {
                  return await http.GetFromJsonAsync<OrderDto>($"/orders/{orderId}", ct);
              }
              catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
              {
                  return null;
              }
          }
      
          public async Task<Guid> PlaceOrderAsync(PlaceOrderRequest request, CancellationToken ct = default)
          {
              var response = await http.PostAsJsonAsync("/orders", request, ct);
              response.EnsureSuccessStatusCode();
      
              var result = await response.Content.ReadFromJsonAsync<PlaceOrderResponse>(ct);
              return result!.OrderId;
          }
      }
      ```
      
      ### Saga / Process Manager
      
      ```csharp
      // Application/Sagas/OrderFulfillmentSaga.cs
      public sealed class OrderFulfillmentSaga(
          IOrderRepository orders,
          IInventoryServiceClient inventory,
          IPaymentServiceClient payments,
          IShippingServiceClient shipping,
          IEventBus events) : ISaga<OrderFulfillmentState>
      {
          public async Task<OrderFulfillmentState> HandleAsync(
              OrderPlacedEvent @event,
              OrderFulfillmentState state,
              CancellationToken ct)
          {
              state = state with { OrderId = @event.OrderId, Status = FulfillmentStatus.Started };
      
              // Reserve inventory
              var reserved = await inventory.ReserveAsync(@event.OrderId, @event.Items, ct);
              if (!reserved.IsSuccess)
              {
                  await CompensateAsync(state, ct);
                  return state with { Status = FulfillmentStatus.Failed, FailureReason = "Inventory unavailable" };
              }
              state = state with { InventoryReserved = true };
      
              // Process payment
              var paid = await payments.ChargeAsync(@event.OrderId, @event.Total, ct);
              if (!paid.IsSuccess)
              {
                  await CompensateAsync(state, ct);
                  return state with { Status = FulfillmentStatus.Failed, FailureReason = "Payment failed" };
              }
              state = state with { PaymentProcessed = true };
      
              // Create shipment
              var shipment = await shipping.CreateShipmentAsync(@event.OrderId, @event.ShippingAddress, ct);
              state = state with { ShipmentId = shipment.Id, Status = FulfillmentStatus.Completed };
      
              await events.PublishAsync(new OrderFulfilledEvent(@event.OrderId, shipment.Id), ct);
              return state;
          }
      
          private async Task CompensateAsync(OrderFulfillmentState state, CancellationToken ct)
          {
              if (state.PaymentProcessed)
                  await payments.RefundAsync(state.OrderId, ct);
      
              if (state.InventoryReserved)
                  await inventory.ReleaseReservationAsync(state.OrderId, ct);
      
              await orders.MarkFailedAsync(state.OrderId, state.FailureReason, ct);
          }
      }
      ```
      
      ---
      
      ## Pattern Selection Guide
      
      | Scenario | Recommended Pattern |
      |----------|---------------------|
      | New project, small team, unclear requirements | Simple layered or modular monolith |
      | Complex domain with many business rules | DDD with aggregates and domain events |
      | High read/write ratio disparity | CQRS with separate read models |
      | Independent team deployments required | Microservices with clear contracts |
      | Rapid feature development focus | Vertical slices |
      | Need future extraction flexibility | Modular monolith with explicit contracts |
      
      ---
      
      ## References
      
      - [Clean Architecture by Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
      - [Vertical Slice Architecture by Jimmy Bogard](https://www.jimmybogard.com/vertical-slice-architecture/)
      - [Domain-Driven Design Reference by Eric Evans](https://www.domainlanguage.com/ddd/reference/)
      - [CQRS by Martin Fowler](https://martinfowler.com/bliki/CQRS.html)
      - [Microsoft .NET Application Architecture Guide](https://learn.microsoft.com/en-us/dotnet/architecture/)
      
  • SKILL.md 2.2 KB
    ---
    name: dotnet-architecture
    version: "1.0.0"
    category: "Core"
    description: "Design or review .NET solution architecture across modular monoliths, clean architecture, vertical slices, microservices, DDD, CQRS, and cloud-native boundaries without over-engineering."
    compatibility: "Best when project structure, service boundaries, or long-term maintainability are in scope."
    ---
    
    # .NET Architecture
    
    ## Trigger On
    
    - choosing architecture for a new or evolving .NET system
    - reviewing layer boundaries, domain boundaries, or service decomposition
    - deciding whether clean architecture, vertical slices, CQRS, or microservices are justified
    
    ## Workflow
    
    1. Start from business capability boundaries and change frequency, not from a preferred diagram style.
    2. Use simple modular monolith patterns by default, and move to microservices only when team autonomy, scale, or deployment boundaries justify the added operational cost.
    3. Apply DDD and CQRS where business rules are genuinely complex; avoid forcing aggregates and command pipelines into CRUD-heavy code with no payoff.
    4. Keep dependencies flowing inward when using clean architecture, but avoid creating extra projects that add ceremony without ownership clarity.
    5. Make integration boundaries explicit: contracts, storage ownership, messaging, consistency model, and observability expectations.
    6. Use `dotnet-aspire` when local orchestration, service discovery, and developer observability are part of the architecture story.
    
    ## Deliver
    
    - an architecture direction that matches system complexity
    - clear project and dependency boundaries
    - migration notes or tradeoffs when changing an existing structure
    
    ## Validate
    
    - the proposed structure reduces rather than increases accidental complexity
    - data ownership and integration paths are explicit
    - the architecture is testable and operable, not just diagram-friendly
    
    ## References
    
    - [references/patterns.md](references/patterns.md) - detailed implementations of Clean Architecture, Vertical Slices, DDD, CQRS, Modular Monolith, and Microservices with C# 12+ examples
    - [references/anti-patterns.md](references/anti-patterns.md) - common architectural mistakes including over-abstraction, anemic domain models, premature microservices, and cargo cult patterns
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related