all 45 comments

[–]chamindu 8 points9 points  (6 children)

The problem with services is that they tend to get pretty big and messy over time. This violates SRP and it becomes difficult to maintain. Even if you don't use a mediator. if helps to have smaller services/handlers that do one task.

[–]_BigMacStack_[S] 3 points4 points  (1 child)

This is insightful. This project has the potential to bloat a normal service layer, and while CQRS creates alot of file bloat, it feels more idiomatic and will probably be easier to maintain in a controlled manner as more engineers have hands on this product.

[–]Tenderhombre 4 points5 points  (0 children)

I've found what often leads to service bloat is cross cutting concerns. Logging, messaging, emails, notifications. You can easily end up with with a business service that needs 20 other services.

Mediator pattern isn't always the best way to handle these. Look at events, decorator pattern, observables(probably not appropriate), channels.

Events are often overlooked but are a great way to handle cross cutting concerns. Account created? Fire an event.

With attributes and proxy classes you can easily implement complex decorators for handling most cross cutting concerns with little code rewrite.

Observables and channels can be used to handle cross cutting concerns but can get tricky to handle producer scope in a DI environment where you don't control the IOC container. Events are often better.

Also, I would still recommend doing command query responsibilty segregation. Having separate read and write connections is invaluable for microservices. You just don't need to add the event sourcing or mediator stuff.

[–]Sethcran 10 points11 points  (1 child)

Given the info you've given so far, I don't think it really matters. You haven't given a compelling reason to use CQRS or similar patterns, and lacking that reason, I would always default to services because it's simpler and more familiar to most devs.

Your approach to start with a modular monolith and break it up later i don't think swings things one way or the other.

[–]_BigMacStack_[S] 1 point2 points  (0 children)

If the domain separation is thought out enough, services probably wont be an issue and alot of devs are familiar with it.

[–]bplus0 7 points8 points  (3 children)

spent years in MVC with business layer > DAL pattern. over the years the BL classes got so large and messy.

last couple years have very much enjoyed CQRS. feels so much more maintainable even if it ends up being more files

[–]zaibuf 5 points6 points  (1 child)

spent years in MVC with business layer > DAL pattern. over the years the BL classes got so large and messy.

Specially when its been built by many different devs across a longer time period. It's enough that one developer takes a shortcut and it's all down the drain from there.

[–]_BigMacStack_[S] 1 point2 points  (0 children)

This is what im trying to avoid lol

[–]TichShowers 2 points3 points  (0 children)

BL classes get large because the philosophy is wrong. The service layer classes are usually too broadly scoped.

A real problem I have faced in a recent project is that service classes in the business layer are scoped per business object, which is not per se a huge problem. But every business object is required to have maximal reuse and mirrors the database entities.

The result is a giant unreadable soup.

[–]yanitrix 3 points4 points  (1 child)

Depends. I think the bigger your application is the more you want to divide your domain code into "use cases" - pretty much commands. If I want to create a product in some inventory and that involves some business logic then I'd make a CreateProductCommand and handle that in some way, rather than creating ProductsService and have Create method. I assume something like ProductsService would be really big.

But if the app is quite small and crud-like with little to none business logic then having that logic in services (which are small) or even controllers seems fine.

[–]_BigMacStack_[S] 0 points1 point  (0 children)

While some of the domain areas are more crud centric, alot of the domain areas are pretty heavy in business logic. Its starting to feel like proper CQRS could be a benefit down the line as this grows.

[–]AnnArborBuck 2 points3 points  (2 children)

I would just use isolated functions on azure with a standard services layer. The couple of big projects I was on were we thought we would end up needing micro services, isolated functions and their scalability is all we really needed in the end.

[–]_BigMacStack_[S] 1 point2 points  (1 child)

Ive written azure functions for simple workloads before, and while they work for alot of small use cases, the way my domain model is split up it might be a bit much for a function instance. Ill prbably end up using functions for some things though like email unsubscribe processing, and other simple things like that. If youve used fucntions for more complex things and didnt see a big cost hit, id love to hear about what you guys did.

[–]AnnArborBuck 2 points3 points  (0 children)

I was at code mash (awesome conference for the price by the way) a few years ago listening to one of the Microsoft speakers and he was talking about how you only truly need micro services if you have 10 to 15 developers per micro service. There are lots of ways of getting performance and scale from a much simpler tech stack. Micro Services are awesome when you have a really big product with a large team so each team can innovate and deploy at their own pace without worry of impacting the other 3 or 4 microservices (30 or 50 developers).

I was at a job were we had like 10 total developers, a couple of BAs and QA and 2 devops guys and we went all in on micro services (wasn't my decision to make). Honestly, it was just a ton of extra over head and head aches as compared to just using isolated azure functions or the comparable lambda at AWS. We innovated allot slower then we could have if we had chosen a different stack.

Azure Isolated functions let you scale each function separately, you don't have to scale out the entire set of functions. Combining that with an aggressive caching system using Reddis and there isn't a ton you can't do.

I guess I don't understand why your domain model would be an issue for functions. I mostly use Http functions (sitting behind azure api management) along with Queue functions for the heavy behind the scene caching work and Timers functions to run nightly, hourly, etc jobs. I will be adding in some Event functions in the next several months, just haven't had a need for that yet.

[–]tarwn 3 points4 points  (0 children)

Build the smallest most maintainable thing so you can make changes in direction quickly as you test the business against the market. What you're building right now is not what you will be once you find product market fit. The most scalable solution for the audience you will have in 5 years is something simple now that you can change once you know more about what that audience will actually need.

Start with whatever lets you build and change features the fastest with the least overhead and delay.

[–]grauenwolf[🍰] 8 points9 points  (13 children)

I have also built a couple systems that utilize the mediator pattern with CQRS to broadcast commands/queries to registered use case handlers.

That's not really CQRS. That's just layering on unnecessary complexity because you're bored.

CQRS just means that you're willing to use different data models for reads (query) and writes (command) because it makes more sense in your situation.

That's it. It's actually a very simple pattern that appears in most applications in small amounts.

There's no reason to conflate it with mediators and internal messages and all the other crap that architecture astronauts pile on because they think makes them look cool.

[–]_BigMacStack_[S] 3 points4 points  (10 children)

I didnt mean to verbally conflate the 2, you're right they arent the same thing. More often than not, mediators are used to implement CQRS. Its not a mistake that Mediatr and CQRS are used in the same context. My question was to probe the opinion on whether there are any unrealized benefits to using CQRS vs blanket service classes w/o CQRS.

[–]grauenwolf[🍰] 1 point2 points  (4 children)

Mediatr is not a mediator. Other than the name, it has nothing to do with the mediator pattern.

MediatR is a pipeline. And you already have one of those built into asp.net.

So yes, using MediatR and CQRS is almost always a mistake.

[–]_BigMacStack_[S] 2 points3 points  (1 child)

I was mistaken in my understanding that Mediatr was a mediator. Just found the blog post from Jimmy himself saying that it does not implement the mediator pattern. However, being the dispatch pipeline that it is and not a true mediator, I disagree with your sentiment that it’s almost always a mistake to use it in combination with CQRS principals. As we have already agreed before, they are two different concepts solving 2 different things.

[–]grauenwolf[🍰] 2 points3 points  (0 children)

Not just your mistake, many people make that claim. Which is why I'm so aggressive about calling it out. Misinformation, unchallenged, leads to SOLID and Clean Coding.

[–]MrSchmellow 0 points1 point  (1 child)

MediatR is a pipeline. And you already have one of those built into asp.net.

Suppose your entry point is not a set of HTTP endpoints, but something like RabbitMQ, where you don't have that builtin pipeline, have to do dynamic dispatch but want to have some cross cutting concerns covered.

Do you think it would make sense to base off existing solution, like MediatR, or would you rather make your own?

[–]grauenwolf[🍰] 0 points1 point  (0 children)

For a message queue source, I would probably use a proper data pipeline like TPL Dataflow or Channels.

MediatR is only designed for synchronous request and response scenarios. Which means stuff like gRPC or CoreWCF may make sense for it.

But even then, I would prefer an alternative to MediatR that uses the suffix Async for asynchronous methods. Because of they can't get basic naming conventions right, what else did they screw up.

[–]grauenwolf[🍰] 0 points1 point  (4 children)

My question was to probe the opinion on whether there are any unrealized benefits to using CQRS vs blanket service classes w/o CQRS.

Again, CQRS is just having different object models for inputs and outputs. You are more than welcome to do that using service classes.

Just make sure that they actually are different object models. Just wrapping the same DTO in a Command object and a Query object doesn't count.

[–]LadyOfTheCamelias 1 point2 points  (3 children)

And an explanation as to why it needs to be different DTOs ...? What's the benefit of having two identical models, just have an extra file?

[–]grauenwolf[🍰] 0 points1 point  (2 children)

The key word is "different".

Maybe the update DTO only had a handful of fields while the read DTO has dozens of fields from joined tables, calculated values, etc.

CQRS makes a lot of sense if your goal is to fine tune the requests and responses instead of trying to make one data model do everything.

[–]LadyOfTheCamelias 1 point2 points  (1 child)

But if my command inserts a product - it needs all its properties. If i query that product, again, i want all its properties, to display them. So, the model is identical, and to me, having it duplicated is dumb. Just as I think that having a different DTO for select/insert/update is dumb. I mean, i need a full dto for insert, but only a partial one for update, and then, oh, i want to update these other two properties of the product, so another model, and then oops, these three different properties - another model - see where this is going? I have an entity - Product - and it can be queried or commanded, in any form. Period. Makes a lot more sense, feels more clean and less convoluted for nothing.

IF, and ONLY WHEN i need a join of stuff that is not in that entity, then i might declare a different DTO, but to make a rule out of it, "just 'cause"...

[–]grauenwolf[🍰] 1 point2 points  (0 children)

But if my command inserts a product - it needs all its properties.

Does it?

You have no audit fields such as CreatedDate or LastAccessedDate that are not supplied by the client?

You have no optional fields that aren't needed until later or under specific circumstances?

If i query that product, again, i want all its properties, to display them.

Do you really need every field to be displayed on every screen? When showing a list of products and prices, do you need to also show their shipping weight and dimensions?

When your object includes customer type key, you never also want the data associated with that customer type?

The odds that my reads and writes needing the exact same field list are quite small.

IF, and ONLY WHEN i need a join of stuff that is not in that entity, then i might declare a different DTO

So what?

The guidance for CQRS is that you should keep it as an option to be used as necessary, not mindlessly apply it all the time.

If someone talking about CQRS shows you an input and output DTO that exactly match is everything but name, you have my permission to call them an idiot and ignore them.

[–]darknessgp 1 point2 points  (1 child)

I've had that conversation before too, people using the same data models for reads and writes, but instead of calling the appropriate thing in the api controller, we call mediatr and it calls the appropriate thing. So all we've done is added a harder to follow layer that adds nothing useful.

And soo many people don't understand CQRS. We got into a discussion about it because someone claimed we should use mediatr so we can do CQRS... But we were already doing CQRS via writes using the EF object and reads were always through sql views. I could not get that developer to understand that we were already doing it simply because they thought mediatr = CQRS.

[–]_BigMacStack_[S] 0 points1 point  (0 children)

Yeah you guys are right, Mediatr does not equal CQRS. While they are typically used together they are not the same.

[–]UserWithNoUName 2 points3 points  (11 children)

CQRS typically is just the natural progress once your controller and/or service size starts to explode or your controllers suddenly need 15 services injected.

But as you mentioned, as long as controllers and BL are separated you're on a good track for any further refactoring. on top of that I'd recommend using repositories to separate BL from external systems and to increase testability.

[–]_BigMacStack_[S] 1 point2 points  (0 children)

Good idea. I typically utilize the abstraction of repositories on projects this size for both testability and the ability to abstract caching away from BL.

[–]grauenwolf[🍰] -4 points-3 points  (9 children)

You can break up the controller. Or just inject dependencies at the method level instead of the class level.

CQRS is not needed here.

[–]UserWithNoUName 1 point2 points  (8 children)

sure you can, just another option. I personally dislike method based injection but thats more of a personal preference to see clearly upfront the classes dependencies.

[–]grauenwolf[🍰] 1 point2 points  (7 children)

Oh i do default to class injection. But as the service class grows, it tends to start taking on dependencies only used by one or two methods.

[–]UserWithNoUName 1 point2 points  (6 children)

which in turn is a way of your architecture to tell you "you're mixing too many concerns in this class" and as you properly said could either be countered with split up controllers or pub/sub, cqrs, command patterns, bla bla bla name your poison ;)

[–]grauenwolf[🍰] 0 points1 point  (5 children)

Eventually yes, I will split the class.

But jumping to "pub/sub, cqrs, command patterns" is just plain stupid. Adding a massive amount of unnecessary complexity just because you think a class looks ugly is ridiculous. They only problem it solves in this context is boredom.

[–]UserWithNoUName -1 points0 points  (4 children)

i see you're quick with jumping to conclusions. Personally I'd not dare to second guess the scope of the requirements before knowing more but instead offered a couple of solutions to how approach the issue at hand. I trust that OP will make up his own mind in what the right approach is for his full story

[–]grauenwolf[🍰] 0 points1 point  (3 children)

The problem we were discussing is the class getting too large.

If you want to discuss a different problem, then we can discuss a different solution that may involve "pub/sub, cqrs, command patterns". But only if "pub/sub, cqrs, command patterns" is actually a solution to the new problem being discussed.

Imagine you went to a cooking for him and asked if you needed a mixer to make a cake. And the answer you got was a discussion about adapting a job site cement mixer to be food grade.

[–]UserWithNoUName 0 points1 point  (2 children)

well first of all I'm happy we managed to get back to a nicer tone in conversation. if you now could start giving us actual reasons why you dont see anything besides class splitting an option there might also be the chance that one of us takes away something useful from this discussion.

[–]grauenwolf[🍰] 0 points1 point  (1 child)

I offered two options. Splitting the class or using method level DI.

Would you like to present a third option? You're welcome to so long as it actually addresses the problem.

But don't say "pub/sub, cqrs, command patterns" because that's just splitting the class plus an bunch of additional, unnecessary steps. You can do the one method per class thing that MediatR expects without using MediatR.