In the first part of this series, we traced how software architecture trends influence our approach to structuring code. Yet, experience reveals that architecture decisions hinge on human factors—team dynamics, company culture, and the software lifecycle—areas where AI falls short. A well-designed architecture must balance technical needs with human realities.
Treat the domain as a flat list of focused functions
Whether you label them as commands, handlers, or services, the core idea remains: organize your domain as a flat list of functions, each with a single, well-defined purpose. For example, a function to create users would handle validation, transformation, and database interactions without branching into unrelated logic. This approach minimizes complexity and reduces the accumulation of technical debt over time.
The specific pattern—be it mediator commands, single-method services, or something else—is less important than consistency. What matters is that every function remains small, testable, and responsible for one discrete process. Over the years, this method has consistently delivered stability in projects of varying scale.
Separate reads and writes in the domain, not the infrastructure
Command Query Responsibility Segregation (CQRS) was originally designed to split read and write operations at the infrastructure layer, assuming most applications handle far more reads than writes. In practice, high-traffic systems that justify this separation are rare outside major platforms. Even in the Czech Republic, with a population of around 10.5 million, few applications reach the scale where such optimizations are necessary.
Despite this, dividing domain functions into distinct read and write functions is still valuable. If your business relies on displaying data to users, those read operations belong in the domain, not an external system. Misinterpreting the domain as strictly for writes ignores the business’s actual needs.
Use higher-order functions to layer complexity
As domains grow, you may need functions that orchestrate others. A higher-order function—akin to a saga pattern—can call multiple domain functions in sequence, provided it strictly uses the functions one level below it. For instance, a second-level function might invoke several first-level functions, but functions on the same level should never call each other directly.
Most projects, even in complex financial domains, rarely require more than two levels of functions. However, in high-complexity environments like travel agency systems, three or four levels may be justified. The key is to enforce clear boundaries and avoid circular dependencies.
Keep the domain monolithic within a single application
A domain should exist as a cohesive monolith within a single application or API. Avoid scattering domain logic across multiple .csproj files unless you’re deliberately extracting a new application to take over specific responsibilities. Modularizing the domain prematurely introduces unnecessary fragmentation and complicates maintenance.
Restrict domain functions to three core actions
Within each domain function, limit yourself to three types of operations, executed in any order:
- Validation: Ensure in-memory data meets business rules before proceeding.
- Transformation: Modify or restructure data to match required formats.
- Dependency: Interact with external services, such as databases or APIs, to fetch or persist data.
These actions keep functions focused and prevent them from growing into sprawling, unmaintainable units.
Leverage EF Core as your abstraction layer
Many applications rely on Microsoft SQL Server or other relational databases, and switching databases is uncommon unless the initial schema is poorly designed. In most cases, abstracting DbContext behind a repository interface adds unnecessary overhead. Modern testing tools allow mocking DbContext directly, eliminating the need for manual abstractions.
Tools like .AsNoTracking() in Entity Framework Core address performance concerns often cited against ORMs. Similarly, patterns like Unit of Work offer little value when DbContext.SaveChanges() already provides the same functionality. Adopting these patterns without clear justification violates the YAGNI (You Aren’t Gonna Need It) principle.
The future of C# architecture lies in simplicity and clarity. By treating the domain as a flat, single-responsibility function list and avoiding premature abstraction, developers can build systems that remain agile, maintainable, and aligned with business needs.
AI summary
C# projelerinizde fonksiyon odaklı domain tasarımı, CQRS’in yenilikçi yaklaşımları ve EF Core’un sunduğu avantajları keşfedin. Kod mimarisini optimize etmek için ipuçları ve stratejiler.