When searching for guidance on .NET solution architecture, you will find countless repositories and resources. Each tends to focus exclusively on their preferred approach, whether it is:
-
Clean, Onion or Hexagonal architecture
-
Tactical Domain-Driven Design
-
Modular monolith
-
Microserverices
The real challenge? Most materials present their chosen approach as the universal solution, overlooking the nuanced reality of day-to-day software development. What is missing is a clear decision map to help you understand when and how to combine these different architectural elements and patterns.
The examples you do find often fall into two extremes: oversimplified demos that donβt reflect real-world complexity, or overwhelming enterprise-scale applications that are difficult to learn from. Even worse, some resources inadvertently perpetuate misconceptions by conflating different architectural concepts.
We have all heard the phrase "it depends" so often itβs become a clichΓ©. Yes, every decision depends on context-but that doesnβt mean we canβt develop heuristics to guide our choices.
This repository offers something different: not just another static collection of patterns, but an evolutionary journey through the software architecture decision-making process. Here you will learn how to evaluate tradeoffs, combine approaches, and craft solutions that meet your specific needs at any given time.
Such an approach plays well with the dynamics of system implementation. Every development project faces a fundamental challenge that is called "_The Project Paradox:
When we start a project, we need to make crucial architectural decisions - exactly when we understand the domain the least. This creates a strong temptation to fall into one of two extremes - overcomplicated trap or oversimplified pitfall.
Too often, we get swept up in tech trends and conference talks, leading us to preemptively adopt:
-
Microservices before understanding our scaling needs
-
Complex orchestration systems
-
Data streaming solutions
-
NoSQL databases
-
Caching layers
β¦βand more, "just in case we need them."
The result? We create our own complexity nightmare. New team members face a steep learning curve, costs spiral without clear benefits, and identifying performance bottlenecks becomes a maze of interconnected systems. We end up solving problems we donβt have yet while making it harder to solve the ones we do.
The opposite extreme, though less common, is equally problematic: starting too simple and staying there too long. This usually manifests as a monolith that grows unchecked until it becomes an unmaintainable "big ball of mud" - where new features simply get piled on without proper boundaries or structure.
What we will show you is an evolutionary approach to architecture - one that grows with your understanding of the domain, and the complexity of your system. This path will help you build systems that are neither overengineered from the start nor too simplistic to scale.
Our approach unfolds like a story across four chapters, each building upon the last.
The entire repostiory reflects years of real-world experience, including the lessons we learned the hard way. Our goal is to guide you toward architecture that grows with your needs β neither too simplistic to scale nor too complex to maintain.
What we cover:
-
Analysis of a representative business domain (Fitness)
-
Domain-Driven Design (strategic and tactical)
-
Architectural patterns selection and evolution
-
Hybrid architecture combining modular monolith and microservices
-
Loose coupling
-
.NET backend implementation with minimal API
-
Architecture decision log
-
Clean coding practices
What we leave to you:
In this chapter, we will show you how to lay the right foundation for your solution architecture. Our approach begins with a single project (Fitnet
) that is thoughtfully organized from day one - but without overcomplicating things.
The key is our vertical slice approach: each business process gets its own namespace, creating natural boundaries while keeping related code together. Think of it as organizing by what your software does rather than by technical categories.
Why this works:
-
Hit the ground running - spend time building features instead of debating project structures
-
Everything for a process lives in one place (like SignContract) - no more jumping between Controllers, Entities, and Services folders
-
Future-proof your code - need to remove or relocate a feature? Just grab its namespace
Our modules talk to each other through a simple in-memory queue - just enough infrastructure to get the job done without overcomplicating things.
More information:
In this chapter, we will tackle a familiar scenario: your initial architecture is starting to show its limits. Your codebase has matured, and reality has begun to diverge from your early assumptions. Some modules are more complex than anticipated, others simpler. Meanwhile, your growing team is wrestling with merge conflicts in your single project.
Time for the next evolution of your architecture. We will split our project into purpose-built projects that match each moduleβs actual needs.
Simple modules stay simple:
-
Fitnet.Reports
When all you need is straightforward data access and presentation? One project could be enough.
Middle-ground modules find their balance
-
Fitnet.Passes.Api
-
Fitnet.Passes.DataAccess
Perfect for modules that need clean data access patterns but arenβt swimming in complex business rules.
Complex domains get room to breathe
-
Fitnet.Contracts.Api
-
Fitnet.Contracts.Application
-
Fitnet.Contracts.Core
-
Fitnet.Contracts.Infrastructure
For those modules where business logic runs deep.
The beauty of this approach? Each module gets exactly the architecture it deserves - no more, no less. Meanwhile, our modules keep talking through the same in-memory queue, proving you donβt need to complicate everything at once.
More information:
In this chapter, we tackle a crucial evolution: knowing when and how to extract your first microservice. We will explore the real-world signals (we call them "disintegrators") that tell you it is time to let a module spread its wings and become independent.
But extracting a microservice is just part of the story. We will also level up our communication game by replacing our trusty in-memory queue with a proper message queue component. This isnβt just about following trends - it is about building the robust communication backbone and ensure redundancy that your growing system needs to communicate with the new microservice.
Along the way, you will pick up key microservice architecture concepts - not just the theory, but the practical knowledge you need to make it work in the real world.
More information:
In the final chapter of our journey, we will show you something powerful: you can grow into a domain model without starting from scratch. Getting there is more like evolution than revolution.
Remember all that work you did early on - breaking down your domain, identifying subdomains, and organizing them into modules? Surprise! You have already done most of the heavy lifting for strategic Domain-Driven Design. Thatβs right - your foundation is stronger than you thought.
Now comes the fun part: tactical DDD. Think of it as adding precision tools to your toolkit:
-
Value Objects
-
Entities
-
Aggregates
-
Domain Events
The best part? You donβt need to apply every DDD concept to get value. It is like a chefβs kitchen - you donβt need every gadget to cook great meals, just the right ones for your dish.
More information:
Each chapter includes static code analysis to maintain code quality β a practice we strongly recommend for production environments.
Note
|
All chapters represent various patterns and practices, but they are not hard and fast rules. Choose the approaches that meet your specific needs, or combine them to create the solution that fits your context. Think of this as your architectural compass, ready when you need it. |
It is time to look at our domain. We have chosen a fitness domain as our example β one that seems familiar at first glance. Most people recognize the basic flow:
-
Getting membership offers
-
Preparing and signing contracts
-
Getting access passes
-
Attending fitness classes
But this surface-level familiarity can be deceptive.
However, to identify the above processes, you usually need to analyze the domain with domain experts and break it down into smaller pieces called subdomains. Otherwise, there is a high risk of falling into a big ball of mud (or distributed mud) where everything is tightly coupled.
Note
|
The traditional domain expert is often pictured as a business representative β someone who lives and breathes the business processes every day. But donβt overlook another crucial source of domain expertise: your developers, especially those who have been in the trenches with legacy systems. Think about it: developers working with legacy code must understand not just how the system works, but why it works that way. They have seen the edge cases, handled the exceptions, and often know which business rules are truly critical versus which ones are historical artifacts. |
What appears to be a straightforward domain actually conceals layers of complexity:
-
Pass expiration and renewal workflows
-
Sophisticated discount strategies
-
VIP member privileges
-
Access control and rejection handling
-
Membership status transitions
While we wonβt model every aspect of a fitness studioβs operations (that would be a year-long endeavor!), we will demonstrate something more valuable: how to identify and separate key subdomains, and how to build extensible components that can grow with your needs. We have carefully chosen examples that are complex enough to illustrate real-world challenges while remaining manageable and instructive.
Ultimately, you should be able to apply similar techniques to your own complex domains, regardless of industry.
Every complex business domain becomes more manageable when broken down into smaller pieces. In domain modeling, we call these pieces Subdomains. Each subdomain represents a cohesive part of the domain.
But how do we identify them?
While there are several effective approaches to domain decomposition, we particularly value two powerful techniques:
-
Event Storming: A collaborative modeling method that helps discover domain events and processes
-
Domain Storytelling: A visual approach to understanding business processes through storytelling
Both techniques help reveal the natural boundaries in your system by focusing on business flows and interactions. We wonβt focus on the details of either here, just the results you can achieve.
After careful analysis, we have identified the following subdomains that represent fitness domain:
Note
|
Watch out! You might have noticed we used simple names like Pass, Offer, and Contract for our subdomains. While these work for our example, they could lead you down the wrong path in real projects. These entity-like names can limit your thinking about what a subdomain represents. Instead, think in terms of business capabilities and processes. For example, instead of Passes, consider Passes Registration; rather than Offers, use Offers Management. This process-oriented naming becomes especially valuable when your domain grows. You might need to split a subdomain into more specific business processes β for instance, breaking Offers Management into Offer Publishing and Offer Advertising. |
While there are many processes in each of the subdomains, we have chosen specific workflows to keep our example focused and meaningful:
Here is where things get interesting: these six processes across four subdomains initially appear independent. However, this isolation represents a common architectural pitfall in real-world applications. To make our example more realistic and instructive, we need to address how these subdomains communicate with each other.
Our fitness studio system comes alive through two key interactions:
-
First, when you sign a contract, the system automatically registers your access pass
-
Second, when your membership nears expiration, it triggers the creation of an offer
These workflows demonstrate how our seemingly separate subdomains need to work together in real-world scenarios.
Now we need to somehow transform our subdomains into a technical solution. It is time to focus on bounded contexts.
To represent subdomains in your technical solution, you need a model. Often, we decide to just create a single, unified model for the entire solution. Unfortunately, this leads in future to a lot of issues because every subdomain operates in its own, and unique context.
Let us explain.
Imagine having two subdomains of Workout Programming and Meal Planning and each operate with the Set term. However, it has very different meanings in each context:
-
In Workout Programming, a it refers to a specific number of exercise repetitions (like "3 sets of 10 pushups")
-
In Meal Planning, it means a predefined collection of meals (like "the basic meal set includes breakfast, lunch, and two snacks")
Using a single model across different subdomains forces your code to constantly check its context before applying business logic. This inevitable path leads to what developers jokingly call "ifology" - a maze of nested if-statements spreading throughout your codebase. Soon enough, what started as a simple intent to reuse code transforms into the dreaded "big ball of mud" or "spaghetti code" - where business rules become so intertwined that making even small changes becomes risky and time-consuming.
Another approach is to divide your system into multiple models. Each model will represent its ubiquitous language, business capabilities, and rules. This approach allows models to be extended independently without affecting the others. This way each model becomes a separate bounded context.
Important
|
Donβt be misled by the word "ubiquitous" in "ubiquitous language" - it is a common trap that catches many development teams. It is only universal (ubiquitous) within its specific bounded context, not across your entire system. |
While working on software systems, you will usually face one of the following cases:
-
Multiple subdomains are represented by a single bounded context
-
Each subdomain is represented by a single bounded context
In our implementation, we have made a deliberate architectural choice: each subdomain operates as its own bounded context, establishing clear boundaries and independence. However, it is important to note that real-world domains arenβt always this neat. Sometimes multiple subdomains naturally group together into a single bounded context. Take, for example, how Progress Tracking, and Virtual Coaching might naturally combine into a "Personalized Training" bounded context.
One of the really useful tools while working on the definition of bounded contexts is Bounded Context Canvas from DDD Crew.
When you are finished defining all the bounded contexts, you should define the communication and integration map between them. You can use a Context Map to do this, but we will not focus on explaining it here.
Think of this repository as a novel rather than a technical manual. Each chapter builds naturally on the last, telling the story of how a real-world application evolves from its first lines of code to a mature, well-architected system. Simply follow the chapters in order, like turning the pages of a book, and see how each architectural decision naturally leads to the next. This "story-first" approach helps you understand not only what we built, but also why we made each choice along the way (supported by the architecture decision log).
This repository is organized with crystal-clear simplicity: beyond this main README and its supporting assets, you will find four key chapters that tell our evolutionary story:
In each folder you will find the same root solution, but expanded:
-
Chapter 1: Your starting point - a simple, initial architecture
-
Chapter 2: Growing pains lead to project separation
-
Chapter 3: Taking the microservice leap and upgrading communication
-
Chapter 4: Embracing tactical DDD in a maturing microservice
Each chapter comes with its own detailed README - think of it as a chapter-specific guide that walks you through running the code, understanding the structure, and grasping concepts unique to that stage. We believe in building on knowledge, not repeating it, so each chapter focuses on what is new while assuming you are familiar with what came before.
Important
|
While you are free to explore these chapters in any order, we strongly recommend starting with Chapter 1 and progressing sequentially. Like any good story, the earlier chapters lay crucial groundwork that makes later developments more meaningful. Jumping ahead might leave you missing important context and concepts. |
Our solution is built with C# and .NET 9, complemented by a carefully curated set of libraries and platforms. We have intentionally kept external dependencies minimal - just enough to avoid reinventing wheels while keeping the focus on architecture. While using third-party tools means trading some control for productivity, we believe it is a worthwhile tradeoff for most business applications.
Application:
Testing:
You can get a solid understanding of Evolutionary Architecture through these videos:
In this webinar, Maciej "MJ" Jedrzejewski gives an in-depth presentation on Evolutionary Architecture. You can watch the recorded webinar at here.
This is a recorded presentation from the Programistok 2023 conference where Evolutionary Architecture was explained in detail. You can watch it on YouTube here.
Explore Evolutionary Architecture visualized with NDepend, with comprehensive dependency and code analysis in the form of an interview with Kamil conducted by Ferry de Boer. Watch it on YouTube at here.
Maciej "MJ" Jedrzejewski | Kamil Baczek |
---|---|
Author of the book "Master Software Architecture", technology agnostic architect, and Microsoft MVP. |
.NET Engineer, software architect, and Microsoft MVP who empowers teams to build better software through solid software architecture, utilising techniques such as Event Storming, Domain Driven Design and various architecture styles and design patterns. |
Really appreciate the repository and the content you are sharing. I think it is really valuable to see the evolutionary approach in practice, and focus on simplicity and pragmatic reasoning and making decisions in software design.
Your work has been super helpful as I dive deeper into this field. Evolutionary Architecture, in particular, is incredible and has really clicked for me thanks to your examples and insights.
I was always concerned about what architecture to use on a project. Sometimes, spending time on researching whatβs right affects my motivation. Your approach which focuses on creating a project with an architecture that evolves resonates perfectly with me. Your efforts in creating such a wonderful resource is greatly appreciated.
Connect with fellow architects and developers in our "Evolutionary Architecture" community on our Discord. Share your questions, suggestions, or feedback about our repository - weβre here to learn and improve together.