<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pathum Kumara</title>
    <description>The latest articles on DEV Community by Pathum Kumara (@pathum_kumara_d43aeb29286).</description>
    <link>https://dev.to/pathum_kumara_d43aeb29286</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3748133%2F24f1256e-60c1-4bef-aaa0-6e9acce62485.jpg</url>
      <title>DEV Community: Pathum Kumara</title>
      <link>https://dev.to/pathum_kumara_d43aeb29286</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pathum_kumara_d43aeb29286"/>
    <language>en</language>
    <item>
      <title>Building Scalable Backends with DDD &amp; Domain Events .NET C#</title>
      <dc:creator>Pathum Kumara</dc:creator>
      <pubDate>Tue, 12 May 2026 14:13:30 +0000</pubDate>
      <link>https://dev.to/pathum_kumara_d43aeb29286/building-scalable-backends-with-ddd-domain-events-net-c-1dak</link>
      <guid>https://dev.to/pathum_kumara_d43aeb29286/building-scalable-backends-with-ddd-domain-events-net-c-1dak</guid>
      <description>&lt;p&gt;Over the last few days, I sepent time refining architectural patterns in a modular .NET backend centered around payroll processing, approvals, and workflow-driven operations.&lt;/p&gt;

&lt;p&gt;One area I focused on heavily was aggregate design and state protection. Instead of exposing mutable collections directly from entities, aggregates internally manage their own state exposing read-only access externally.&lt;/p&gt;

&lt;p&gt;private readonly List _monthlyAllowance = new();&lt;/p&gt;

&lt;p&gt;public IReadOnlyCollection &lt;br&gt;
              MonthlyAllowances =&amp;gt; _monthlyAllowance ;&lt;/p&gt;

&lt;p&gt;This prevents external code from bypassing aggregate rules;&lt;/p&gt;

&lt;p&gt;employee.MonthlyAllowances.Add(...)&lt;/p&gt;

&lt;p&gt;while still allowing controlled state transitions through aggregate methods:&lt;/p&gt;

&lt;p&gt;public void AddMonthlyAllowance(MonthlyAllowance allowance) &lt;br&gt;
{ &lt;br&gt;
  if(_monthlyAllowances.Any(x =&amp;gt; x.PayrollMonth == allowance.PayrollMonth &amp;amp;&amp;amp; x.SalaryItemId == allowance.SalaryItemId)) &lt;br&gt;
 { &lt;br&gt;
   throw new DuplicateMonthlyAllowanceException(); &lt;br&gt;
 } &lt;/p&gt;

&lt;p&gt;_monthlyAllowances.Add(allowance); &lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Another major refinement was moving workflow reactions out of aggregates and into domain event handlers. Rather than aggregates directly triggering notifications or workflows, aggregates simply raise business events.&lt;/p&gt;

&lt;p&gt;public void AddMonthlyAllowance(MonthlyAllowance allowance) &lt;br&gt;
{ &lt;br&gt;
  _monthlyAllowances.Add(allowance); &lt;/p&gt;

&lt;p&gt;AddDomainEvent(new MonthlyAllowanceSubmittedForApprovalDomainEvent(Id,    allowance.Id, EmployeeName)); &lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The aggregate only represents business behavior. Reactions happen externally through handlers:&lt;/p&gt;

&lt;p&gt;public sealed class&lt;br&gt;
MonthlyAllowanceSubmittedForApprovalDomainEventHandler&lt;br&gt;
    : INotificationHandler&amp;lt;&lt;br&gt;
        MonthlyAllowanceSubmittedForApprovalDomainEvent&amp;gt;&lt;br&gt;
{&lt;br&gt;
    public async Task Handle(&lt;br&gt;
        MonthlyAllowanceSubmittedForApprovalDomainEvent notification,&lt;br&gt;
        CancellationToken cancellationToken)&lt;br&gt;
    {&lt;br&gt;
        await _notificationRepository.AddAsync(&lt;br&gt;
            new HrNotification(&lt;br&gt;
                "Allowance Approval Required",&lt;br&gt;
                $"Approval required for {notification.EmployeeName}",&lt;br&gt;
                "HR_MANAGER"));&lt;br&gt;
    }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This separation dramatically reduces coupling and keeps aggregates focused on business invariants instead of orchestration concerns.&lt;/p&gt;

&lt;p&gt;I also revisited feature-based application organization. As systems scale, grouping code by business capability rather than technical type becomes significantly easier to maintain.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;Application&lt;br&gt;
 ├── Commands&lt;br&gt;
 ├── Queries&lt;br&gt;
 ├── Handlers&lt;/p&gt;

&lt;p&gt;feature-oriented organization tends to scale better:&lt;/p&gt;

&lt;p&gt;Application&lt;br&gt;
 └── EmployeePayrollProfile&lt;br&gt;
       ├── Commands&lt;br&gt;
       ├── Queries&lt;br&gt;
       ├── EventHandlers&lt;br&gt;
       ├── Validators&lt;br&gt;
       └── DTOs&lt;/p&gt;

&lt;p&gt;Another area that improved domain clarity considerably was replacing primitive-heavy models with explicit value objects.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;public int cYear;&lt;br&gt;
public int cMonth;&lt;br&gt;
public double Amount;&lt;/p&gt;

&lt;p&gt;the model becomes much more expressive:&lt;/p&gt;

&lt;p&gt;public PayrollMonth PayrollMonth { get; }&lt;br&gt;
public Money Amount { get; }&lt;/p&gt;

&lt;p&gt;with validation centralized inside the value object itself:&lt;/p&gt;

&lt;p&gt;public sealed class PayrollMonth : ValueObject&lt;br&gt;
{&lt;br&gt;
    public int Year { get; }&lt;br&gt;
    public int Month { get; }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public PayrollMonth(int year, int month)
{
    if (month &amp;lt; 1 || month &amp;gt; 12)
        throw new DomainException(
            "Invalid payroll month.");

    Year = year;
    Month = month;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Approval workflows were another interesting area. Instead of tightly coupling approvals to controllers or services, workflows are modeled through state transitions and domain events:&lt;/p&gt;

&lt;p&gt;allowance.Approve();&lt;/p&gt;

&lt;p&gt;AddDomainEvent(&lt;br&gt;
    new MonthlyAllowanceApprovedDomainEvent(...));&lt;/p&gt;

&lt;p&gt;This allows notifications, audit trails, projections, escalations, and future integrations to evolve independently without modifying aggregate behavior.&lt;/p&gt;

&lt;p&gt;I also spent time evaluating architectural tradeoffs between:&lt;/p&gt;

&lt;p&gt;in-process messaging and distributed messaging&lt;br&gt;
modular monoliths and microservices&lt;br&gt;
domain events and integration events&lt;br&gt;
feature-based organization and layer-only organization&lt;/p&gt;

&lt;p&gt;One thing that consistently becomes clear in larger backend systems is that many scalability and maintainability problems originate from coupling and boundary design long before infrastructure becomes the bottleneck.&lt;/p&gt;

&lt;p&gt;For modular monolith architectures in particular, using MediatR with domain events provides a clean middle ground: maintaining loose coupling and workflow flexibility without introducing distributed-system complexity too early.&lt;/p&gt;

&lt;p&gt;Current stack and concepts:&lt;br&gt;
.NET 8 • EF Core • MediatR • DDD • CQRS-style patterns • Modular Monolith Architecture • Event-Driven Workflows&lt;/p&gt;

&lt;h1&gt;
  
  
  dotnet #csharp #softwarearchitecture #ddd #backend #cleanarchitecture #modularmonolith #mediatr #cqrs #enterprisesoftware
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
