Decouple systems with ASP.NET Core Hosted Service

The story of Hosted Service and Channel to build decoupled systems.

I have been working on a project for around two and a half years. The high architecture is the Event Sourcing-Like. I do not dare to claim it Event Sourcing because it will lead to many debates.

Here is the overview. There are 2 data stores. The primary one is an SQL database storing all events. This is the fact, the truth of the system. The second one is a Cosmos database. This stores the projected views from the events. So the writes go to the Event Store. The reads go to the Cosmos store.

Architecture

The write side composes of Application Services. The read side composes of Projections. We have to send events to the Projections to project them and store the normalized views in Cosmos. The communication has to be asynchronous.

The normal reaction is some kind of a queue. However, we do not want to go that far if we can handle everything inside a single Web Application. Our application is not at StackOverflow scale so a single Web Application instance should be enough. There is a lot of data for sure, but the request rates are reasonable.

Projections as Background Service

The Projections must be run as a background service alongside with the WebApi. With the ASP.NET Core, it is fairly easy with Hosted Service. All good stuffs are documented by Microsoft. Go and check it out. Good stuff! Guaranteed!

We also use Hosted Service for other purposes such as Deleting Files.

Ok, we have the WebApi (the application services) and a background service, how are they communicating to each other?

Channel

It is well-documented, well-explained here DevBlog Channel, or a wonderful blog from Steve Gordon. These links cover everything you need to know about Channel.

To solve the communication issue, we use Channel to create a channel from Application Services to Projections. It is a multi-writers, single-reader channel. If we need a two-way ticket, another channel is created. We also have it for the scenario where the Application Services have to wait for the Projections.

It works beautifully.

Code

So it is time to see some code. All the code is on GitHub

I started a new project with the .NET 6.0 (the latest one). It is cool to use the latest even thought I do not use any special feature in the project.

Let start with a simple stupid API

[ApiController]
[Route("[controller]")]
public class CountersController : ControllerBase
{
    private readonly CountersService _service;

    public CountersController(CountersService service)
    {
        _service = service;
    }

    [HttpPost]
    [Route("/increase")]
    public async Task<IActionResult> Increase()
    {
        await _service.IncreaseCounter();
        return Ok("Have a good day!");
    }
}

The API exposes an endpoint that will make some changes to the system. The controller then dispatches the actual work to a service CountersService. This approach keeps the controller simple, stupid, and clean.

The CountersService handles the logic and finally publishes events.

public class CountersService
{
    private readonly EventChannel _eventChannel;

    public CountersService(EventChannel eventChannel)
    {
        _eventChannel = eventChannel;
    }
    public  Task IncreaseCounter()
    {
        // Execute whatever the business logic here.
        // And finally, write an event package into the event channel.
        // Imagine there is an actual framework here which will publish events.
        // Those events are then packaged into EventPackage and dispatch
        return _eventChannel.Write(new EventPackage
        {
            EventName = "CounterIncreased"
        });
    }
}

That is the end of a service. It does a bunch of stuff and finally publish events (EventPackage).

Who is going to handle those package asynchronously? The Projection Service

public class ProjectionBackgroundService : BackgroundService
{
    private readonly ILogger<ProjectionBackgroundService> _logger;
    private readonly EventChannel _eventChannel;

    public ProjectionBackgroundService(
        ILogger<ProjectionBackgroundService> logger,
        EventChannel eventChannel)
    {
        _logger = logger;
        _eventChannel = eventChannel;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Keep running until asked to stop
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await foreach (var package in _eventChannel.ReadAllAsync(stoppingToken))
                {
                    // Include the logic to proceed a package here.
                    // A package typical contains the actual event (or events) that the projection needs to project
                    _logger.LogInformation($"Package received: {package.EventName}");
                }
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Bad thing happened");
            }
        }
    }
}

It picks up event packages from EventChannel one by one and proceeds.

The final piece that connects them is the EventChannel

public class EventChannel
{
    private readonly Channel<EventPackage> _channel;

    public EventChannel()
    {
        var options = new BoundedChannelOptions(10_000)
        {
            SingleReader = true
        };
        _channel = Channel.CreateBounded<EventPackage>(options);
    }

    public IAsyncEnumerable<EventPackage> ReadAllAsync(CancellationToken cancellationToken)
        => _channel.Reader.ReadAllAsync(cancellationToken);

    public async Task Write(EventPackage eventPackage)
    {
        if (await _channel.Writer.WaitToWriteAsync())
        {
            await _channel.Writer.WriteAsync(eventPackage);
        }
    }
}

public class EventPackage
{
    public string EventName { get; init; }
}

And finally, register those components with ServiceCollection (DI framework in .NET Core)

public static class ServiceCollectionExtensions
{
    public static void RegisterComponents(this IServiceCollection services)
    {
        services.AddHostedService<ProjectionBackgroundService>();
        services.AddScoped<CountersService>();

        services.AddSingleton<EventChannel>();
    }
}

Once run, you should see this in the console

And that is it. I have a decouple architecture that will work.

Observation – Watch Out Boundaries

When I first started my software development career I think writing software was hard. And it is true. However, the definition of writing software at that time was different. What I really meant was my code met the functional requirement (and it was not always true) and ran. That was when I did not see in production. So everything was working fine in my machine.

Over the time, I have had chances to bring my code into production and seen them running. No surprise that they did not work well in production. Every developer knows the famous "It works well in my machine". And it might work well in test. But it always has problems in production.

Why? There is no single answer for that problem. Software are developed by developers with different level of skills, experience, intelligent, … Even the team has the most talented developers in the world, their products still have bugs. So I am not trying to find a solution for that problem. Instead, I embrace and observe the fact. There is no silver solution but there are tips and tricks to prevent as much as possible.

From my own experience, from Pluralsight courses, from youtube, … from any source I touched, I want to document what I have observed. I do not intend to go in the detail of each item. Rather I want to have a list and some explanations, references. The detail varies project by project.

SQL

If you build applications with SQL Server as data storage, watch out 2 common unbounded patterns

  1. Select N+1
  2. Select * without top n

Asking Google for "Select N+1" you will know what it is immediately. There are detail explanation with code example. Usually developers that have worked in ORM know it very well.

The second watch out is a bit tricky. That is when your applications issue a query to SQL in this pattern

SELECT * FROM dbo.Employee WHERE [Predicate]

In the test environment, there is no problem. However, in the production, the data is huge and that query might return millions of records.

These days many applications do not talk to the database directly. Instead there are ORM/LinqToSQL in between. And this piece of code is not uncommon

var employeesByName = ctx.Employees.Where(x => x.Name.StartWith("Smith")).ToList();

Some ORMs might put a limit on the generated queries. What developers need to do is to review all the generated queries.

The rule of thumb is that always control the number of returned records. Put the max on everything.

Connections

There are many kinds of connections that applications make – connect to the database, connect to external services. When making such those calls, there are some watch out

  1. Timeout: Make sure a timeout value is set on everything. Usually the modern frameworks have default values. Just make sure there is and you are aware of them.
  2. Close connections properly. Just imagine what happens if you have your door opened? Bad things happen.

JSON – Serialization and Deserialization

JSON is cool. Developers work with JSON everyday in one form or another. Many take it for granted. We rarely pay attention to the size of data. I once experienced such a problem here – hidden cost of an architecture.
So if you have to explicitly use JSON directly in your code, ask these questions

  1. Do I have to use it? Is there any other options?
  2. What size? Is the size under controlled?

Some might argue that RAM is cheap. So why should we care too much about the size? Yes. RAM is cheap but it has limit. Once it reaches the limit, your application will freeze or crash. And if your applications are running on the cloud, everything your applications consume, there is cost involved.

Enumerable, List, ForEach

Does developer write code without the usage of loop? Have we ever wondered how many items are there in a list? When talking about a list, we should be aware that all items are stored in memory. So the size really matters here even it is trivial.
Another trap is at the Enumerable. Enumerable represents a sequence that mostly expected to iterate only once. By nature, we do not know the size of a sequence (There is no Count property on the IEnumerable interface). Therefore, when calling .ToList(), be aware of all the nasty things can happen.

I brought them here for reference. It might not a thing that takes down production. But it is nice to be aware of as well.

There might be more about boundaries to watch out. Those are what I have come up so far. What’s yours?

Unit Test from Pain to Joy

Recently I have made an improvement in a project regarding its unit test. The improvement has a huge impact on the system in a good way. It also has a huge impact on my thinking, my philosophy about unit test, again, in a good way.

The Story

I have been working on a huge, complex codebase. It is still written with .NET 4.0, about 6 years old. Part of the system is WCF service employed CQRS style. The code has its existing unit test. And we have added more. The tests have both integration tests and mocked tests.

Integration tests, in our context, means starting with the top-most layer (presentation layer at WCF service) down to the database, and then getting data back from the database using Query Handler or Repository. In short, it is a complex testing style. And to my opinion, it is not good. I would not have done that.

Mocked tests, in our context, means mocking all the dependencies. In short, all the interfaces are mocked. Even for the domain objects, we also create proxy objects and mock their properties, methods. It turns out a big mistake.

Most of the time, we are mocking instead of testing state. There are a few problems (pain) with our approach.

First, it is hard to write. To mock dependencies, you have to know all the dependencies. Which means developers have to read the implementation code, figure out how they are interacting with each other, figure out what methods call on which interfaces. Those are not really fun. And they do not bring any real value.

Second, it is fragile. Whenever we add code or refactor a piece of code, the unit test breaks. Because the unit test assumes that a number of certain methods are there and that they are called in a specific order. Which is, in our context, not suitable.

Third, it is damn hard to write tests to verify a bug fix.

Root Cause

How did the hell on earth I end up in that situation? Everything has its own reasons. And I want to figure out mine.

I have bought this excuse for so long. And I was happy with it, unfortunately

Because the codebase is complex. The design was wrong. We do not have time to redesign it.

The fingers were pointing to the other guys, other developers. No, it was not true.

What role have I played in that mess?“, I asked. Oh, turn out I play a very big role. After reflection, here are some reasons to me not doing well on my that area.

Wrong Mindset

A long time ago, when I started knowing and writing unit tests, I was sold the thought that we have to cut the dependencies, especially the database dependency. And with the raising of mocking, the promising of TDD, I mocked (in the hope of cutting dependencies) as much as I could. It works in many scenarios. However, because I believe it is the right thing, I forget to ask right questions.

Not Ask Right Questions

I just wrote tests without asking right questions.

What am I testing here?

Kind of a stupid question, but very powerful. Depending on types of application, on the architecture, answers are different. Because of my wrong mindset, I focused on how instead of what. Answering that question allows me to analyze further, allows me to actually look at the system in a systematical way, instead of a theoretical way.

Solution

First, I decided to throw away what I thought I know about Unit Test. Here are what I want my Unit Test should be

  1. Easy to write, easy to understand from code.
  2. Resilient to refactoring. Do not have to modify unit tests when using another interface in the implementation code. In short, the tests should be there to guarantee the code correctness at maintenance phase.

While writing this post, I created a github repo, welcome to DotConnect.

What are We Testing?

Such a simple but powerful question! However, I sometimes forgot to ask. We know that we should write unit test for our code. Have we ever considered to answer that question properly? Take an example, given that we will build a simple web service (WCF) to CRUD an Account into the SQL database. What are we going to test? Each will have a different answer, thus, drive their unit test implementation.

When asking that question, the important is to remove the term Unit. I find it is a trap. When that term presents, my mind is trapped in defining what unit is. Therefore, I forget the purpose of my testing.

From my own opinion, at the abstract level, I will categorize them into 2 categories 1) Functional Test and 2) Architecture Test

Functional Test

That are tests to govern the correctness of the system. For this kind of test, we have to define clearly what is the final state.

InputOutput
The simple diagram of all processes

To implement a proper test, one must clearly define the Output. Some common outputs are 1) Database, 2) Filesystem

To define a good output, we have to define the proper scope (which comes later in this post)

Architecture Test

For some systems, architecture is important. Let’s say all the call to the Database must go through a repository. Or that the Controller (MVC application) must delegate the work to the next layer (such as Service or Command/Query Handler).

Usually, we use Mock to accomplish the testing goal. Because we do not really care about the actual implementation. We care about the sequence of calls.

What are Dependencies?

Dependencies must be listed out explicitly. At the minimum, there should be a simple diagram like this

Dependencies
High-level dependencies of the system. Each box (such as Google) is a dependency

And do not go into the detail of those dependencies. Better keep the high-level view.

What is the Scope?

Without a scope, things get messy. A proper, explicit said scope will help to define the Input and Output. I made a mistake at this question so I defined a wrong scope. I, once, defined scope at the Project level. I had a unit test for Command Handler project, which will mock the dependency to the Repository project. Then I had another unit test for Repository project. They, first, looked logical and reasonable. However, with the tested of the time, it proves I was wrong.

Once I realized it (and that is why I write this post), I defined the scope at Command Handler level only, remove the concept of Repository test. Which allows me to define the Input is the Command, and the Output is the changes in the database.

This is a game changer step for me. For years, I have been focusing on the term Unit. The problem is that it is hard to define the unit. Will it be a function, a method? Will it be a class? or Will it be an assembly? Well, I do not know. Better I just choose to forget about them.

So what do I have so far in my toolbox regarding unit test? Here they are

  1. Ask the question: What am I testing?
  2. Explicitly list all dependencies at high-level
  3. Define testing scope
My Unit Test toolbox
My Unit Test toolbox

Applicants

Back to the story, the system I have been working with is a complex system, a data-driven system. The data is back by SQL Server. From the architecture point of view, it is WCF service with CQRS architecture. When a command is executed, there are a bunch of things involved, the domain, the domain service, the external services (AD FS, payment service, …), … eventually, the data is saved into SQL Server database.

From the command side:

Q: What am I testing here?

A: Save data correctly in the database

We should not care about what domain involved, what domain service called, … They are internal implementation. And they are changed frequently. We chased the wrong rabbit hole.

From the query side:

Q: What am I testing here?

A: Get data from the database correctly.

We should not care about how data is filtered, how data is combined, … They are internal implementation. And the same reasoning goes as in Command.

In both cases, the test will give an input and verify the output. However, we still have a problem with dependencies. A big change in my mindset is that I no longer see the database as a dependency. Rather it is a part of the internal system. Why? Because it is an essential part. It can be set up locally and in CI environment. Therefore, my definition of dependency is

Dependency is external systems that we do not control. That they are hard to set up. That we do not care about their implementation. Database should NOT be one of them.

How to mock those dependencies of not using a Mocking framework? A good practice is that for every dependency, there should be a Proxy (the Proxy design pattern). The proxy implementation is injected at the runtime with the help of an IoC framework such as Windsor Container. For Unit Test, I create a fake implementation and tweak as I want.

I took me a little while to set up all those things. But it works. It gives a lot of payoffs.

Implementation Detail

[PS: This section has not finished yet. However, this post has started for a while. I think I should publish it and save the implementation detail for another post.]

To implement this kind of test, I need to interact with the database to insert and get the data regardless of the system logic. This separation is very important. To accomplish this, I use Linq To SQL.

Due to the confidential contract, I am going to create a simple demo instead of using a real application. Let’s create a simple User form MVC application.

[Code]

Having a separated assembly allows me to isolate the changes. The Linq DBContext allows me to interact with the database as I need.

All the tests have a pattern

  1. Assumption: Prepare data. This step insert the data into the database for the command to execute
  2. Arrange: Prepare the command to verify.
  3. Act: Invoke the command handler correspondent to the command. Each command has its own Command Handler.
  4. Assert: Verify the result. Use Linq To SQL to get the data from the database and verify our expectations.

Instead of repeating the steps, I create a Strategy pattern

When Mock?

There are still scenarios where the Mocking is a perfect solution. Usually, it is the top layer of the system. Let’s take MVC (WebAPI) as an example. In my opinion, the Controller should be as light-weight as possible. What a controller should do are

  1. Validate input
  2. Prepare a request (can be a Command or a Query)
  3. Dispatch the request to the next layer. If the system employees CQRS, that layer is usually a Command Handler or Query Handler.
  4. Return the result

Which steps should be mocked? The step #3. What are we testing? We test to ensure that the Controller sends correct command/query to the next layer. We test the behavior of Controllers. The mock might be a perfect fit for Architecture Test.

[Code]

What’s Next?

The implementation detail for all the stuff I write here. Now it is time to let this post out so I get something DONE.

Deliver This Feature, Agile Said. Hmm, What About?

Agile is a good methodology to get things done. I buy the philosophies behind it. I think it can be applied in many areas, not just at work, not just at building software. To me, Agile is a tool, a meant to help me get the job done. In software development, I have heard people talking about these beautiful things.

  1. Agile team is self-organized
  2. Deliver working software (features) every sprint.
  3. No upfront design.
  4. Hmm, do we need an Architect? Do we need a Team Leader?
  5. Embrace changes
  6. ….

Many fantasy things. When I first heard about them, I thought

Wow. That’s so cool. Everything looks perfect. Hmm, if everything is perfect, then there is a hidden problem.

Unfortunately, I felt something was missing. Later on (at least up til now), I have found many. Basically, all those things are correct if, and only if, you have a perfect Agile Team. In reality, we all know that human is the root of everything. And having a good team is, well, just a dream. We have to face that truth. Do not try to assume we have a good team at hand. It just does not work that way.

Let’s take a small area from that fantasy: Add a new feature.

The Story

Good morning SWAT Agile Team, in the sprint we will deliver a new cool feature: Delete (soft delete) user. The feature will allow the administrator to delete a user. Users are marked as deleted but not removed from the data storage.

With these facts

  • The system has 10 of features built in.
  • There are features associated with User: Create, Edit, Manage Order, …

The wishes

  • The new feature will not break the existing ones.
  • Reuse the code as much as possible.

The untold facts (which will be discovered when writing user stories for the feature)

  • Some existing features will be impacted. Cannot delete users having orders. Cannot edit a deleted user.
  • Code Quality: Cyclomatic Complexity (CC) must be under control.

If you have been in such a situation, please share how you will handle it.

The Problem

Developers, Testers when joining a so-called agile team, how much have you known about Agile? When a project starts, developers are sold a plan of getting small deliveries, increasements. No upfront design. No architecture. Hey! That is the waterfall, folks, an Agile-men said.

There are many requirement meetings, discussions, … Everything goes well as planned. The team has a clear set of user stories. Estimation is done. Agile is on set.

Good! Ladies and gentlemen, where do we write the first line of code? Serious. Where do we write? Not mention how. To make thing super clear, where/how do you write the first line of production code? Just to opt out the Unit Test code.

A harder question. How do we code in such a way that in the middle of the sprint (or next sprints) the requirements change? How do we embrace the changes?

As the system grows, the harder to add a new feature.

The Solution

Hell No. I do not have a solution. However, I do have some proposals.

Infrastructure and Architecture

No matter what fields we are in, to embrace changes, to stand still, we need support from Infrastructure and Architecture. Without a good foundation, we just build a house on the sand. A single wind can blow it away.

We need an architect. We need a good, stable foundation. If there has not, we must build one.

We need an architect to define system boundaries, to separate the logics into proper concepts, to decide the architectural style, to decide design patterns. A good architect will limit the impact when implementing a new feature or modifying an existing one.

For legacy systems, a good architect can help decouple the dependencies, create boundaries. It is much harder than in the green field projects. But it can be done and can improve over the time. Otherwise, every single change is a risk.

Agile Mindset

People are the core of everything. Members decide the success or failure of the project, of the team. What is their mindset? We bring Agile in to increase the flexibility, to embrace the changes, … None of them will work if members think

  1. I must have a clear requirement to start with.
  2. My job is just coding or testing
  3. Who cares about the design. I just need to code this function. I change only a small part of the code.
  4. Hey, I just add a new method for me. Isolated. No breaking change.

Still the old mindset. I am not saying that mindset is wrong. But it does not fit the new environment either.

I would suggest the first thing we should do is to train our member; to get them adapt with the new mindset; to install the agile mindset into their subconscious mind. It is not easy, not at all.

A few things we can train developers

Architectural Thinking

There is no clear definition of Architect. However, everyone can think architecturally. Every developer, paid for a job, must be able to write code. They should not stop there. Instead, ask these questions

  1. Can I read my code after 3, 6 months? How about a year?
  2. How many ways can break the code?
  3. How easy to change? What if clients want to change XXX?
  4. Is it easy to understand?

What kinds of architectures that support well to embrace changes?

It is a long road. It is worth a try. Oh, wait a minute! Which means we need an architect. Yes, Sir! We need an architect or one having architecture skill in the team.

There is an argument saying that the architect will setup the system, build the framework, make some training sessions, and finally handle over to the team. Good luck! Soon enough, the framework will break. Things will tear apart. Why? Because of the lack of quality of maintainers, the development team.

Coaching

We cannot change someone else mindset. It does not work that way. However, we can show them the right mindset. We can consistently tell them the right mindset. Coaching approach might work. That is you do not train or tell someone what to do. No! They decide what they want to do and how they will do it. We, the team leader (I hope you have one), will

  1. Give some suggestions, show the direction
  2. Encourage
  3. Motivate

Will It Work?

I do not know. But if your current team does not work as you expected, maybe you should try it. There is no silver bullet. Try and Improve!

In discussions, every leader seems to know that people are the most important asset. However, when it comes to daily work, the reality might not the same. How much do we care the most important asset? What have we done to improve that asset?

Agile or waterfall, no matter what methodologies we use, we are a team works on a mission. We want to get that mission done and we want to be proud of what we have done.

Should we need an Architect in the team? Yes. Definitely. Then help everyone think as an architect.

Should we train our people? Yes. Definitely.

To win the war, you need good soldiers!

Happy Sunday! Thank for your reading.

Code Duplication is a Myth

I know that code duplication is not good. We try to avoid code duplication. In a perfect world, you do not have code duplication. What annoys me is that we seem to forget to ask some critical questions to ourselves.

  1. Is it really that bad in all circumstances?
  2. What might be worst if we try to avoid code duplication?

Let’s take a typical web application where you have features to add and edit a user.

With 2 separated models

    public class CreateUserModel
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public DateTime? DateOfBirth { get; set; }
    }

    public class EditUserModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public DateTime? DateOfBirth { get; set; }
    }

Or one single User Model?

    public class UserModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public DateTime? DateOfBirth { get; set; }
    }

In single user model, if the Id is 0, assume that is creating a new user.

In the real world application, the model is complex. I make this example to demonstrate the point: There are many duplications in 2 models. The question is whether it is good or bad?

Well, it depends … on the context.

Why do we see it duplicated? Mostly because the code looks the same. I, however, want to look at another angle, from the purpose and context angle.

In term of business view, create and edit a user are 2 different features. They operate on the same data object (User). But they are not the same. Let’s assume that we have a web application. There are 2 screens, one for Create and one for Edit.

        public IActionResult CreateUser(CreateUserModel model)
        {
            return View();
        }

        public IActionResult EditUser(EditUserModel model)
        {
            return View();
        }

        // Reuse the same model for 2 different features: Create or Edit
        public IActionResult SaveUser(UserModel model)
        {
            return View();
        }

In the SaveUser action, if the User.Id is 0, use the Create logic, otherwise, use the Edit logic. So we can put aside the logic.

Initially, both Create and Edit screens look the same. They operate on the same piece of data. The single model sounds a right choice. However, I will choose the 2 models. I do not see them duplicated. I do not see benefits of unifying them.

Here are why

  1. Each is an independent feature.
  2. They have different input and output. Sooner or later you will end up some information on a feature not make sense on another.
  3. By separating them, we can have 2 teams (groups) work on 2 features without stepping on each other toes.

Ok then, when will it make sense?

I will consider a duplication when

  1. A piece of code that carries logic. Not just data or POCO.
  2. Independent of context. The refactor code (as a result of killing duplication) might depend on the input parameters. However, keep an eye on the number of parameters and make your instinct decisions base on your experiences.
  3. Easy to understand. There should be a very limit of condition/switching in the implementation.

From my experience, killing duplication is not an easy task. It requires a lot of skills

  1. Skill to ask the right question about the value of the code, the logic of the code, … One should ask themselves serious questions.
  2. Skill to read the code and get a brief of what it does.
  3. Skill to capture just enough to refactor out.
  4. Skill to refactor.

Did I tell you that Naming is one of the hardest tasks? Given that you have a piece of duplicated code, what would you name them?

I fell in love with the slice (with Feature approach) architecture when I first saw it. I saw its value when traditional architecture caused my headache when the code was out of control. Especially, when I have to develop a new feature. The biggest fear when adding new features is the chance of breaking existing features, aka causing regression bugs. With the feature approach, we can limit the risk.

Every approach has its pros and cons. We, developers, have to weigh them and pick the one that fits most.

There are some valuable links from experts.

Jimmy Bogard SOLID Architecture.

Steve Smith ASP.NET Core Feature Architecture.

Application structure: Concepts and Features.

Isolated Feature from Ayende.

Or checkout Jimmy repo here. A wonderful resource to learn how to build a web application with the Slice + MediatR + Feature.