"Clean Architecture" and indirection. No thanks.

Поделиться
HTML-код
  • Опубликовано: 25 ноя 2024

Комментарии • 252

  • @MilanJovanovicTech
    @MilanJovanovicTech Год назад +140

    I appreciate your commentary and review Derek! I think there's much to learn from it - mainly how "silly" the whole idea is.
    CA purists will still advocate for this approach, so that's why I decided to show it. But as I said, it's not my preferred way of doing things.

    • @adrian_franczak
      @adrian_franczak Год назад +41

      So why you create content on something you don’t recommend? Maybe changing the title to “how it shouldn’t be done” will be enough - idk

    • @adambickford8720
      @adambickford8720 Год назад +19

      @@adrian_franczak Even if you don't like a tech/paradigm/etc you should understand it and what the tradeoffs are. You might decide you like parts, but not to the extent they take it, or it may just solidify you opinion that the tradeoffs of your tech stack make more sense for you.

    • @adrian_franczak
      @adrian_franczak Год назад +3

      @@adambickford8720 I know that that’s why I’m reading a lot of stuff from different points of view but I wouldn’t promote topic where I don’t agree with something

    • @MilanJovanovicTech
      @MilanJovanovicTech Год назад +54

      @@adrian_franczak I don't see anything wrong with discussing implementations. For someone else, this is a great approach.
      It's my freedom to talk about anything on the channel - even things I don't agree with.

    • @adambickford8720
      @adambickford8720 Год назад +5

      @@adrian_franczak It would make you a better engineer. One of the best ways to make sure you *actually* understand something is to teach it in good faith. Use Cunningham's Law to your advantage.

  • @pillmuncher67
    @pillmuncher67 Год назад +141

    I'm regularly astounded by the amount of ceremony that is present in enterprise programming.

    • @CodeOpinion
      @CodeOpinion  Год назад +63

      I think the trap developers fall into is applying patterns/principles without context and understanding why those patterns/practices exist. And the not using their context in that decision. eg, I can't have data access here because it can't live in this layer.

    • @marna_li
      @marna_li Год назад +2

      @@CodeOpinionExactly. That is my opinion as well. I’m currently in a project that has a service layer with classes just to group stuff.

    • @KyleSavant
      @KyleSavant Год назад +3

      @@marna_li like a facade pattern or for some other reason?

    • @marna_li
      @marna_li Год назад +10

      @@KyleSavant It is where business logic is supposed to be put. To separate it from the presentation logic. Controller calling the services.
      Putting stuff in services is quite arbitrary. It is not like the services really have any meaning. They are more like modules, in something resembling feature folders.
      I wouldn't structure my project that way. Services, if they should exist, should have purpose. I'd rather have commands and handlers.

    • @KyleSavant
      @KyleSavant Год назад +6

      @@marna_li I’m in a similar situation. I misunderstood your first comment.
      The organization I work for has massive classes called “Managers”. It is just a dumping ground for methods that vaguely do work around some conceptual noun (ex. - PersonManager). There could be many methods in this manager, many with different dependencies.
      Imagine how big this constructor is and how coupled it becomes to unneeded classes. It just makes my skin crawl every time I need to work with it.

  • @Fafix666
    @Fafix666 Год назад +67

    You should run a code review series where people show their approach, and you point out the good and the bad.

  • @leonardomangano6861
    @leonardomangano6861 Год назад +46

    The main issue here is that a lot of developers bought the idea that you can decouple things that are naturally (or semantically) coupled.

    • @vincentcifello4435
      @vincentcifello4435 Год назад +7

      Exactly!.
      Worse still is the indirection, masquerading as "decoupling" , that results in the hidden coupling of things that are naturally not-coupled.*
      * (see Milan's in process Domain event video where separate aggregates are ultimately saved in the same transaction).

    • @leonardomangano6861
      @leonardomangano6861 Год назад +2

      @@vincentcifello4435 You are right bro

  • @marna_li
    @marna_li Год назад +24

    Yeah. You are not eliminating coupling, just moving it somewhere else. It doesn’t matter if it lives in a separate assembly. That will also make stuff messier. I’d use EF directly in request handlers within vertical slices. Only extracting stuff to classes when it makes sense to.

  • @superpcstation
    @superpcstation Год назад +65

    I would love some code review videos. Also I'd love to see how you'd fix this problem

  • @andreiguzovski7774
    @andreiguzovski7774 Год назад +5

    Depending on such abstraction decouples you from your persistence provider(in this case EF Core). Application no longer has to reference EF Core. Moreover it enables you to create several infrastructures(if needed) and easily change it in the future. One of the reasons why teams stick to their ORM provider despite improvements in others is because they neglect this step. The same teams also can't use different ORMs for reads and writes for the same reason. Dapper for queries and EF Core for commands is worth considering.

  • @leuquim
    @leuquim Год назад +18

    I appreciate the "code review" styled video as the stacked opinions help to think about a problem while taking a couple steps back, opposed to just the direct "information absortion" mode we fall into when watching a video. A part of me relates to Milan because I have a fear of doing things "wrong", both for my perception of the code and to mitigate future (unknown) issues. Another part of me relates to Derek because experience tells me I tend to over-engineer projects in a way that rarely benefit anyone in the real world.
    I think specific architecture patterns or concepts are an easy way to get started and build acceptable solutions to a problem, but ultimately there's much more basic underlying concepts that take experience to be confident enough to use as the main guides in our decision making.

    • @CodeOpinion
      @CodeOpinion  Год назад +3

      I think the trap is applying patterns/principles/etc as rules without understanding the underlying problems they are trying to address. Then taking that underlying problem and understanding in your context if it's valuable. There are always trade-offs and you need to be able to evaluate them. I alluded to that more-so at the very end.

  • @alexanderpabinger3960
    @alexanderpabinger3960 Год назад +1

    I love this video and your comments. We talked about this kind of stuff 5 months ago: How can we share queries? How can we abstract the data access for unit tests? We decided to not share queries at all, instead depend on the DbContext (as you said there is no value in using an IDbContext where DbSets are exposed), because each query is so different and the result is mapped to it's own purpose. For the command-side, we decided to use DataAccessors (how we call them, doesn't really matter) and they can be easily mocked. They are also never shared between different command handlers. Thanks you your useful thoughts and videos!

  • @banatibor83
    @banatibor83 Год назад +3

    The guy in the video what you reviewed almost reached perfection IMO. As he said he refactored the code to be more vertically sliced but exposing some of the entity framework defeated the purpose, but I think he wanted to keep it a somewhat more appealing length. If he would have transformed the result from the db queries into some domain specific object that would have made a true separation. With true separation you can switch out the whole persistence layer without disturbing the domain layer. If you go the extra mile and actually implement a strategy pattern you can mix persistence layer solutions and still providing a unified interface towards the domain layer.

  • @MiningForPies
    @MiningForPies Год назад +14

    SingleOrDefault doesn’t throw an exception if there isn’t a result.

    • @sanjayidpuganti
      @sanjayidpuganti Год назад

      Yes. If there are more than 1 it does

    • @MiningForPies
      @MiningForPies Год назад +8

      @@sanjayidpuganti exactly, so he’s broken the original intent of method.

    • @adrian_franczak
      @adrian_franczak Год назад +1

      exacly!

    • @atechdude
      @atechdude Год назад +4

      The behavior of SingleOrDefault can be summarized as follows:
      If the sequence contains exactly one element, that element is returned.
      If the sequence is empty (contains no elements), the default value for the element type is returned.
      If the sequence contains more than one element, an InvalidOperationException is thrown.

  • @llindeberg
    @llindeberg Год назад +5

    Its for test faking! Not that I agree with it necessarily, but i do miss it sometimes instead of using in-memory database for tests. Would öove to hear your comments

    • @anthonymorgan2001
      @anthonymorgan2001 Год назад +2

      Seconded. Can we see an example of unit testing the logic in the handler without using this?

    • @Timelog88
      @Timelog88 Год назад +1

      Yeah I also generally wrap the efcore dbcontext in my own wrapper class because it's impossible to fake itself. What I mostly find disturbing in the example is the sheer amount of interfaces. It's so useless to make an interface type for single implementations.
      Most of the time when I ask people why they do it the answer is because they want to mock it. But why would you ever want to fake more then the external infrastructure? Sociable tests are a thing, and in my experience worth the tradeoff of having multiple tests fail if you break your own code. Since I started doing TDD and sociable tests the amount of interface types I use has dropped significantly.

    • @llindeberg
      @llindeberg Год назад

      ​@@Timelog88 To take it to the extreme, one could do with only e2e-integrationtests I guess. But unit tests with as many dependencies mocked as possible isolates the error for quicker debugging. There's an extreme of that side too, though, mock C#/.NET? But I see your point, tradeoff worth considering. I'm sad Microsoft are hesitant to recommend/expose it.

    • @Timelog88
      @Timelog88 Год назад +1

      @@llindeberg Well personally I stopped using mocks entirely. I just do pure state based tests that test behavior and abstract away all implementation details from the tests feasible. The way I do that is a fairly novel way of testing introduced by James Shore called Nullable Infrastructure. This means that I limit the use of layers in my code, and the amount of dependencies in my code.
      The tradeoff? Test code in my production code. But as a counter argument to that, Microsoft does it as well, fe. NullLogger, NullStream and UrlTestEncoder. Main difference is that I use Embedded Stubs.

  • @adambickford8720
    @adambickford8720 Год назад +10

    Enterprise code is different. When a project lives many years and has multiple 'generations' of devs, you do things that are 'dumb' otherwise. It's easier to find corporate drone devs that can recognize and churn out more dumb patterns than devs who will grok and truly 'own' the app.

    • @evancombs5159
      @evancombs5159 Год назад +4

      This was what I was thinking too. There are things I do that the value is in trying to guide future devs towards better code that I know if I let them to their own devices they would create crap code.

    • @6stringmonk
      @6stringmonk Год назад +5

      This comment is spot on in my experience. I work somewhere where it is common to have a large codebase existing for 2 decades. I've used this "query object" pattern on a few projects over the past 5 years and they've been the easiest apps to maintain and add cross cutting concerns to even though much of the code is redumbdant. It's kind of like N-Tier chopped down to its bare minimum along with favoring SRP over DRY. It's overkill for very small projects but it actually helps coding momentum in medium-sized+ projects where there are a lot of people coding but you are not sure where all the data access and cross cutting concerns are going to land.

    • @carlosbaldwin6335
      @carlosbaldwin6335 Год назад +1

      this comment is gold, after two years working in a project with multiple devs i think this architecture allow us to do things quickly and feel realy natural any change. It improve the development cicle.

  • @edgeofsanitysevensix
    @edgeofsanitysevensix Год назад +4

    I really don't get why some devs insist on using MediatR. To me it offers almost nothing since seperation can be acheived easily without it. Maybe if I was dispatching commands to a handler which drops a message onto a message bus? If it's just seperating controller layers from DB/Service layer then I don't see the point.

    • @yardeZ93
      @yardeZ93 Год назад +1

      This is not the main idea of this tool.... What about pipeline behavior? I personally find that feature very powerful and I believe it is the main purpose of the library and not that API/Application separation.

    • @edgeofsanitysevensix
      @edgeofsanitysevensix Год назад

      @@yardeZ93 Well fine. But I do see it used 'because its cool'

  • @dagochen
    @dagochen Год назад +4

    7:15 An OrderService should return Orders, because its domain logic. It should not return something that can be sent out. That's what the handler should handle!
    7:40: Exceptions are not made for control flow. Validate your id before doing anything!
    18:00: Splitting logic into seperated interfaces for each already seperated handler adds complexity, but no value.
    To really reach seperation of database/persistence from usecases:
    In the Handlers, call a domain service. The domain service uses a repository, which is implemented in the persistence layer and gets the DBContext injected.
    Result: Domain service can be reused in several different handlers to get, create, update delete your domain objects. Service makes sure, everything is valid/consistent. Repository takes care of persistence and uses EFCore or whatever to achieve that.

    • @gbroton
      @gbroton Год назад

      You are confusing a domain service with a application service. Domain services does not use repositories. In fact anything related to domain doesn't.

  • @rhtservicestech
    @rhtservicestech Год назад +2

    10:15 The EF SingleOrDefaultAsync does not throw an exception if there are no records. It returns null, thus the "Default". When looking for a specific ID, it is best use to SingleOrDefault vs FirstOrDefault. If there are multiple rows with the same ID, FirstOrDefault will allow the operation to be performed, which can cause data issues.;

    • @proosee
      @proosee Год назад

      Well, I assume everyone has constraint on ID field (since "ID" stands for "identity"), so this case is a bit exotic, but you're right that "Single..." should be used just because it express your intentions better.

  • @hektonian
    @hektonian Год назад +4

    Or I could just implement the query handler where my DbContext is to avoid that silly leaky interface business altogether.

  • @awright18
    @awright18 Год назад +3

    I really enjoy your take on things, and this type of video. I feel like the Milan's video that was pretty balanced at least in offering different options, and explaining some of the tradeoffs involved in the approaches although I don't believe it answered your question of "what does this buy you?". Also as Milan states below it is a lot of what people might see in the wild, wether or not its a preferred approach. To extend your thoughts a little further, I do see mediator or frameworks like it used often in web applications and treated like an entry point when typically the actually endpoint is the entry point in those situations I have the same thought. What is mediator actually adding besides indirection? To be fair I used to program like this until I realized as you said the single member interfaces are often simply a function. The purpose of many applications is to handle specific use cases, and its not often that we need to have more than one way to handle them at runtime. This reminds me of the memes about jr / mid / sr level engineers where the jr does something simple to solve the problem as they become more experienced (mid level) they make more complex solutions, and then after gaining more experience (sr level) they regress back to the simple solutions with the understanding of why simple solutions are often best.

    • @CodeOpinion
      @CodeOpinion  Год назад +1

      What's the value of mediatr type libraries? Depends where it's being used. It some places it might be useless, others it can be helpful. For example, if you were using aspnet core and you specifically want to remove aspnet core dependencies from your code. Meaning, are you creating a web application or an application. It's used over an integration boundary. So assuming you don't put anything aspnet related in your request objects. Meaning, you're using it at the outer most I/O boundary that you might not control and convert it to something you do control. Let's say you have a request/handler that is used in a a controller as the entry point. You also have another entry point that lets say has to periodically pull data from an external system, translate it and call that same request/handler. In both situations you're taking the top level entry point and translating that in your application request. Can you do this without Mediater, absolutely, it just hides the execution, which then adds things like behaviors/piplines etc. If you're not doing any of that, does it provide you with any value?

  • @nyonyo3553
    @nyonyo3553 Год назад +16

    Let me put a handler in your handler, so I can handle the requirements your handler should handle, but wait there's more!
    I've hidden your DbContext behind an interface that is just a copypasta of the DbContext itself. And why? I have no idea really.

    • @CodeOpinion
      @CodeOpinion  Год назад +6

      Because they don't want to couple directly to the DbContext but rather an interface they own that lives in the Infrastructure layer. But you're still coupled to it because you're exposing the DbSet. I also don't get it.

    • @markovcd
      @markovcd Год назад +1

      Cargo cult programming.

    • @dariogriffo
      @dariogriffo Год назад

      Exactly like exposing Iqueryable to be able to do .Include

    • @pilotboba
      @pilotboba Год назад

      @@CodeOpinion The IApplicationContext should actually use IDbSet and not DbSet. I agree with that assessment. Only the EF Contracts should be a application layer dependency, not EFCore proper.

  • @phantastik3389
    @phantastik3389 Год назад

    This comment is in regards to a generic repo; when I create a generic repo. My "get by id" usual looks like this =>
    Task getFirstOrDefault(Expression filter); I have a few like methods like that I use so I understand where he's coming from.

  • @jwbonnett
    @jwbonnett Год назад +9

    I think it's not really about the abstraction but limiting how and where you can implement things, it means that there is less chance of you messing something up (the intern problem) if you use the current architecture layout. Especially is such large systems. It's a little more work, but you get used to where everything is, so moving things out of the way isn't really a problem.

  • @alexandru-mihailadam8798
    @alexandru-mihailadam8798 Год назад +3

    We would really like to see a clean architecture from your point of view, in any language you would like to.

  • @szotsmiklos8549
    @szotsmiklos8549 Год назад

    This sort of indirection can make sense if we also want to have different compilation units for application logic and database access. CA on steroids is Diamond Architecture, where the core of the project doesn't depend on any infrastructure lib. In CQRS, you'd want commands and queries (as classes) to be next to each other so it's easy to see all features/capabilities of the system. Still all repository implementation along with 'xyzQueryServiceImpl' would go to a DB project. Why? Because some languages are slow to compile (scala, rust) and a larger monolith's compile time can be sped up by this approach

  • @buarki_
    @buarki_ Год назад +3

    Ain't quite sure if I'm properly dressed to witness such code formalism :)
    btw, looking forward to the day LOPJB (Layered-Oriented Programming Just Because) will no longer be so appealing for newbies/(almost) mid engineers. Lately in my job we released a dumb service that just acts as a CMS offload to offer some lower latency for two app-specific queries. Guess which "architecture' one guy from my team just started applying to it (just because)?

    • @CodeOpinion
      @CodeOpinion  Год назад +4

      Did you just make up LOPJB? Either way, I'm using it and will give credit.

    • @buarki_
      @buarki_ Год назад

      @@CodeOpinion lmao yes, it just popped up in my head here, feel free to spread the word against LOPJB

  • @jamesmussett
    @jamesmussett Год назад +3

    The levels of indirection I've seen over the years is really starting to bug me. It's got to a point now where I even see MediatR as unnecessary in some use cases and it's just easier to have a single class for the endpoint itself rather then having any kind of indirection.
    (I say endpoint, can be using FastEndpoints, controllers, or any other kind of endpoint construct. As long as it's a class with a single method / purpose)

    • @pilotboba
      @pilotboba Год назад

      The Endpoints are a UI/Presentation layer. How does the endpoint query the data? Are you coding to an implementation in the endpoint?
      What if you had a second Presentation layer, say a graphql server. You would have to write the query all over again?
      I don't think there is a simple, single, correct answer her for every use case and application.

    • @atechdude
      @atechdude Год назад

      I don't use Mediator :)

    • @jamesmussett
      @jamesmussett Год назад

      @@pilotboba I don’t disagree with the fact that certain use cases will require a different approach. But as ti mentioned, it certainly doesn’t require libraries like MediatR. Thinking about all the use cases upfront certainly helps eliviate having to re-write everything later on, but I appreciate that requirements can always change.
      From my personal experience, endpoints are designed for specific scenarios in which the database queries are normally optimised and tailored for. Query Language APIs like graph QL will most likely have to have it’s own queries written for it anyway. Adding any kind of shared logic and indirection between then is more likely to cause issues in query performance and just add unnecessary complexity.

    • @pilotboba
      @pilotboba Год назад

      @ti Guess you didn't understand my "what if". I wasn't saying you are designing for the future, I am saying your current design INCLUDEs 2 presentation layers. The read model should be available to both presentation layers rather than implementing it twice.
      But, sure, if you know it will only ever be an API and you want to implement the read model in the endpoints, go for it. I am usually more pragmatic than dogmatic.

  • @darylclarino5439
    @darylclarino5439 Год назад +3

    Early on, I enjoyed adding patterns whenever I could to a project that I was working on, but as time went by I prefer my codebase to be as lean as it can be and only add abstractions when I truly need it. The CA pattern is a consistent way to implement a project, but I feel that it has this underlying assumption that a layer might be swapped out in the future. I understand the persistence layer "might" get swapped, but i'm on the fence about the application layer, do we really need to have a dedicated "Application" and "Web" or "Api" layer? In most cases an Api project will always be an Api project, so why not just define whatever is in the Application layer in the Api layer.

    • @CodeOpinion
      @CodeOpinion  Год назад +1

      It's not about swapping out in the future, it's about coupling to abstractions rather than implementations and having a direction of dependencies. Where things go wrong is when people take a template or a formula for doing that and not taking their context into consideration.

    • @darylclarino5439
      @darylclarino5439 Год назад

      @@CodeOpinion i see what you mean, and i agree sometimes people just use the patterns for the sake of using them

  • @dii2
    @dii2 Год назад +7

    I like this video review. I'm a big advocate of using EFCore/Dapper/etc directly in the command handler directly as opposed to abstracting it into some IRepository class (some argue for testing purposes). If you are truly worried about unit/integration testing, EF Core provides an In-Memory database that you could use in your test project. At the end of the day its all about exercising pragmatism for various use cases.

    • @irvingceron1016
      @irvingceron1016 Год назад

      That's cool, EF really has it all it seems

    • @Greenthum6
      @Greenthum6 Год назад +4

      Don't use EF's in-memory database for testing. Use the real database engine to make sure your queries really work.

    • @davideastman5806
      @davideastman5806 Год назад +1

      It should be noted that Microsoft explicitly discourages testing using the EF Core In-Memory database. From the docs: "While some users use the in-memory database for testing, this is discouraged."

    • @irvingceron1016
      @irvingceron1016 Год назад

      @David Eastman
      Oh lol, why do people do it then?

    • @juniorzucareli
      @juniorzucareli Год назад +1

      ​@@davideastman5806 Yes, using inmemory database is not recommended.
      But, people add indirection by arguing that it's for testing, to mock.. Using inmemory, this argument no longer makes sense.

  • @luizfernandopereira5120
    @luizfernandopereira5120 Год назад +1

    I got a bad feeling when I see this abstractions, especially when we try to abstract the DataSource with Services/Repository patterns. I understand you want to have only one way to writing/structure your code base inside the applications so use the Mediator even though its just a thin layer for some cases but can serve as a agreggator for others its ok in small applications or modules. Still would simplify your life by making a vertical arquiteture and only abstract your outboundaries .

  • @megaFINZ
    @megaFINZ Год назад +1

    I don't get the idea of using MediatR just for cross-cutting concerns. You can just use decorators instead.

  • @brandon9247
    @brandon9247 Год назад

    Great video! I would have liked to see as side-by-side of what would be more favorable and meaningful. If the takeaway was to "leave it as is", does it imply there was no problem at all initially? If not, how do we identify when there is a problem and when there isn't? Yes. The details about coupling was a step in the right direction, but I need a "to do" and "not to do" side-by-side for it to sink in.

  • @maurosampietro9900
    @maurosampietro9900 Год назад

    Based on the article: “repository pattern is simple but yet misunderstood”. I’m tempted to let my web services (fastendpoints) directly use EF. I also want to completely avoid dtos and mappings. I’d play around with the serializer (eg: custom jsonconverter) to cut out unwanted data if linq select in anonymous types is not possible . What your thoughts?

    • @carlosbaldwin6335
      @carlosbaldwin6335 Год назад

      dto's and mappings give you an order to manage you data. When you work with a lot of people, you need to understand really fast things.

  • @Greenthum6
    @Greenthum6 Год назад +3

    Great code review and I fully agree on your points. If you want to use Clean Architecture pattern and hide the persistance layer's implementation details then do it properly. In this example EF Core's DbApplicationContext is exposed as-is and the caller in application layer can misuse it freely (just cast the result to DbApplicationContext and off you go). Also greate insight on dropping out the cancellation token without a reason to keep the code nicer.
    I think the root cause may rely in the original Clean Architecture code example where the author did just this. I don't know why EF Core dependency was added all the way to domain layer, but this idea has been copied by many without understanding the consequences. I have used mostly Dapper with repository pattern and then you really need to think about the layer separation when applying clean architecture - you need to actually fetch or store the data before returning the response.

    • @pilotboba
      @pilotboba Год назад +1

      "If you want to use Clean Architecture pattern and hide the persistence layer's implementation details then do it properly."
      What's properly? That's the $100 question here.
      " I don't know why EF Core dependency was added all the way to domain layer,"
      I don't think this was done. What are you talking about? Are you talking about the IApplicationContect interface created in the application layer? That can be done by only depending on EFCore contracts.

    • @Greenthum6
      @Greenthum6 Год назад

      @pilotboba Referring to Jason Taylor's "clean architecture solution template": You are right, EF was not added into the domain layer there but the application layer instead.
      IApplicationContext has IDbSet, which is a hard EF dependency.

    • @pilotboba
      @pilotboba Год назад

      @@Greenthum6 I also think Jason addresses this when he talks about it and it is a pragmatic decision he made. I generally agree with him. The overhead of wrapping the ef interfaces so you can remove a soft dependency on a contracts project is not worth the time, complexity, and extra layers.
      I think any clean architecture templates out there are guidance and not hard and fast rules. Use what you like, throw away or modify the rest. That's how I approach it.

  • @zfold4702
    @zfold4702 Год назад +2

    There is definitely value to it. Consider the Order Query requires to combine results from multiple queries to produce the final result, your design is open to introduce that glue in thin query handler while keeping the query details in the respective Service class. You won't see value in vanilla use cases but this helps in complicated enterprise applications where data needs to be assembled from multiple queries and sources.
    In real world, scenarios can begin vanilla and get complicated as business evolves. So following something like this helps.

  • @sj82516
    @sj82516 Год назад +1

    Appreciate this kind of video to show your different opinion and thoughts. :)
    I try to apply CA (without CQRS) and I did put query into repository layer to keep everything fit into CA as possible. But I didn't put one query into one class.
    Good to know CQRS and different opinions.

    • @sergeykichuk2586
      @sergeykichuk2586 7 месяцев назад

      That’s good approach but the problem is you can still have CQRS and you don’t need these query handlers you can inject your repositories directly!

  • @lexuantruong3835
    @lexuantruong3835 Год назад

    I think we can move the handlers of the read side to another project instead of developing a new abstraction layer In the project, we can use many ways to read data quickly and easy to test. Additionally, application project, not dependency persistence library

  • @darylolson5089
    @darylolson5089 Год назад +5

    I really like this style of video. The review provides another point of view that helps me think of the nuances in the various approaches to application design and implementation. I would love to see more of these.

  • @br3nto
    @br3nto Год назад +1

    16:24 if you have a class (or interface) with one method, you have a function. I think you just described MediatR lol. Thought it’s more like a closure, as the constructor closes over the variables used in the Handler.

  • @44Bigs
    @44Bigs Год назад

    11:41 this is probably the worst offends, but SO common: adding all these layers, but with leaky abstractions everywhere. The result is tightly coupled code spread across multiple classes and assembles.

  • @colton2432
    @colton2432 Год назад

    Trying to summarize your thoughts at the end to better understand: because you're already implementing a CQRS pattern each Read Model is specialized and adding indirection to the persistence layer isn't providing value because IF the underlying persistence implementation changes the specialized Read Model may be irrelevant and because it's specialized and will likely have limited usages?
    However on the command/write side it could be more valuable because we may need to query the aggregate root in order to validate or apply a state change which is a domain centric action instead of a specialized usage based on the persistence implementation?

  • @huguesviens
    @huguesviens Год назад +2

    I'm only halfway where you don't see the value. There is indeed a value, there is no EFCore dependancy now in the application layer. It enables to change the ORM provider somewhere down the line with no impact to your application layer. Do we really need this level of abstraction ? it is debatable but not valueless.

  • @nickolaki
    @nickolaki Год назад

    I love these style of videos, as a mid ish level software engineer my next steps to improving are self reflection on the methods and tools I've picked up over the last couple of years.

  • @mihais2911
    @mihais2911 Год назад +1

    At 13:10 is a bit confusing what you're saying about CancellationToken. Could you please confirm that what you meant is that it should really be passed on and not remove it? Because you seem to be saying that and then finish by saying that "it's kind of the indicator like oh it feel uncomfortable". Thanks!

    • @CodeOpinion
      @CodeOpinion  Год назад +6

      Yes, it should be getting passed through and used. What I mean is that it was removed, probably because it felt odd to have to keep passing it from layer to layer. And that "oddness" is the indicator that you have a lot of indirection. Removing it's usage is hiding the "oddness".

  • @ghevisartor6005
    @ghevisartor6005 Год назад

    uuuhmm i had a project in blazor server, there is no intention to make a mobile app or some other kind of front end...so i have the dbcontext directly in the blazor component for fetching and saving stuff. I cant see how this is bad given the requirement of the project. If i will ever need to make some page render in web assembly mode i will move the methods in an api just for that page needing of client side performance. Maybe i'm not writing big enough projects...

  • @madfish1973
    @madfish1973 Год назад

    Great video! For me it's about decoupling the query handler from the storage layer and technology. We're using EF SQL today, but in six months we may use CosmosDB (especially when we consider the polyglot nature of CQRS projections). Having non-leaky injected interfaces implemented in their own assemblies allows us to do this without altering UI or logic layers.
    Since these interfaces should not leak technology types, vocabulary or concepts, we should make them non-persistent in nature. EF supports state, other storage layer technology does not.

    • @madfish1973
      @madfish1973 Год назад

      I'm a huge fan of my EF models being internal to the storage assembly, enforcing clean tech agnostic interfaces.

  • @Veretax
    @Veretax Год назад

    What confused me was his abstraction was still using the DB sets transforming them into the objects that you might want from a testability perspective this throws me a little bit because if you're wanting to not depend on the database for testing then you need to be able to create mocks somewhere

  • @iankirkpatrick2022
    @iankirkpatrick2022 Год назад

    I really appreciate your videos Derek. I had a couple thoughts of my own here that I have wrestled with and want to get your opinion (or other people's here) on.
    I have made a habit of always hiding EF from my business layer (where my query and command objects live if I'm using CQRS). I do this because of 3 different types of experiences I've encountered that made me decide to do this. My main question is, am I overreacting from these experiences (I know that word overreact is subjective, sorry)
    1. Poorly designed database schema where I want to hide the details of tables and queries out of the high level areas my app (the business layer).
    I've worked on many projects where the database was designed poorly or without foresight and we didn't want to take time re-designing it (and therefor performing data migrations at deployment time). I'm not just talking column name changes. I've had to work on projects where they stored two different types of entities in the same table that were similar but different and used in two completely different contexts. I wanted my domain to represent them with 2 different classes... not even having a base class in common cause they weren't ever reasoned with as similar entities... yet my EF model had to be the same cause they were the same table. I also needed 2 different sets of queries (use these where clauses to get records of these types but these other where clauses to get records of the other type).
    I really did not like seeing the complexity of "these are the same models... but oh wait, they're totally different." in my business logic. It was too low level for the business layer which I wanted to be higher level.
    2. Storage location switch.
    I've had a couple experiences where we use to cache a set of data from an API in the database so we had the business level go directly to EF to get the data... then we decided to cache in redis... suddenly I found myself spending a whole month updating references and query operations to use a redis client instead which, if we had abstracted the querying out, it probably would have been half a day or at most a couple days.
    Worse, suppose we did do a table split like what I described above in the first example... depending on the complexity of the queries to that table just to differentiate between the 2 tables, that change could take a while and be very prone to missing references or updating some incorrectly.
    3. Hiding EF specific operations (i.e tracking nuances).
    I've had lots of times where I've had to do some wierd logic in the EF context to deal with nuances in tracking and sometimes cachinng... I don't like putting code like "Add this entity, but check to see if a related entity is already tracked cause if not, you gotta attach that first... oh wait, i've already tracked a duplicate instance of that entity from another query so I gotta swap this instance out for that one". Logic like that doesn't belong in high level logic like the business layer in my opinion.
    For reasons like these, I've just made it a rule for my projects to always just hide EF completely so I don't even have to worry about those issues. I don't even allow myself to add a package reference to EF in my business assembly.
    Am I overreacting? How else could I address these issues if I allow EF to exist in my business layer?

    • @CodeOpinion
      @CodeOpinion  Год назад

      No, not overreacting. I can't express this enough, it all comes down to coupling. If you have specific data that's going to be used in a high number of places (likely queries), than you likely want to expose that behind some API that you can control. Writes are often limited to a select number of actual usages. If you're using an aggregate, maybe its only there. If you're using transaction scripts, maybe it's just a few places that perform a write. In those cases, abstracting your persistence has a very different value than the earlier mention of a query that has a high degree of coupling. It's about where you need control.

  • @piromanaBG
    @piromanaBG Год назад +1

    If you expose DbSets or IQueriable, you are doing something that is not Clean!
    Having the Infrastructure Layer on top of other layers is beneficial because you decouple your app logic from the infrastructure wiring code.
    If you add on top some bootstrap layer, your web application will be oblivious that it is a web app (e.g., you can extract your Controllers in a class library).
    And you can design your business logic like all the data you need is in memory. The result is code more in line with the OOP and SOLID principles. The idea is to design your app like your data is in memory. And your system to speak the business language.
    The idea of using Repositories is to use them to make queries through Aggregate Roots, but returning the whole Aggregate is unnecessary. This way, your Aggregate root can force all invariants. And you have a single flow point between different Aggregates, which leads to better (in my opinion) database schema design. The Entity from one Aggregate should not have a foreign key to the Entity from another Aggregate. These foreign keys are made through the Aggregate Roots.
    Bootstrap Layer -> ASP app
    Infrastructure Layer -> Persistence etc. (here, the Repositories are implemented)
    Web Layer -> Controllers and MediatR
    Application Layer -> CQRS, Repositories Interfaces
    Domain Layer -> SOLID OOP (Entities, Aggregates, etc.)

  • @asdqwe4427
    @asdqwe4427 Год назад

    I fully agree with you, but man is this an uphill battle in 90% of organisations. You’re doing the lords work!

  • @thedacian123
    @thedacian123 Год назад

    So in a nutshell instead wrapping each query into specific service/abstraction would be ok that in each Meadiator query handler to write query directly as simple as possible using a ligther tool the ef as Dapper? Thank you!

  • @awmy3109
    @awmy3109 Год назад

    You shouldn't unit test with a concrete instance of DbContext. That's integration testing.

  • @CryptoWulfApp
    @CryptoWulfApp Год назад

    How do you handle scenarios where you want to reuse a Query?
    The way I pick was to move the code to a service and used this service in both Queries but I started without one first.
    One other solution could be to orchestrate everything in the Controller but then you cant return Viewmodels from your queries.

  • @brakenthemole2377
    @brakenthemole2377 Год назад

    It would be great to see what your thoughts are on the alternative.

  • @Galakyllz
    @Galakyllz Год назад +4

    I definitely enjoyed this style of video. With regards to the content, there's nothing worse than being excited about a new project, you start abstracting out the different functions, and then realize that you've mapped one-to-one-...to-one all the way down to the database. That said, I think storing anything but a one-liner in a handler leads to bad places, but the amount of indirection applied to this specific project was too much. Once you start creating abstractions of functions (`ExecuteAsync` in this case), you've lost me.

  • @andersborum9267
    @andersborum9267 Год назад +1

    Developers should generally start returning the result of e.g. queries as opposed to assigning them to a variable and then just returning the variable; guys, it's not adding any value assigning stuff to a variable and then on the following line, returning that particular line. Your method (that encapsulates the query or operation) should relay the intent of the contents of the method.

  • @PaulSebastianM
    @PaulSebastianM Год назад +1

    I always love to learn from more senior devs. Trust your elders and all that😂

  • @temp50
    @temp50 Год назад

    4:20 Yeah this is why the onboarding process for a new colleague usually takes like half a year ffs! :) Nobody will understand the reasoning later on, especially if it is not written down.
    But obviously it will not be written down since the code should be self-explanatory, nobody's wiring code-related documents only high level ones.

  • @MarcusTheDorkus
    @MarcusTheDorkus Год назад

    Indirection without a very clear purpose makes code so hard to follow. Especially when you're creating a new interface and injecting the implementation through a DI framework. You end up needing to run the code to figure out what concrete types everything resolves to.
    And if anyone needs another reason to argue against indirection for indirection's sake, keep in mind that there are performance penalties for it. Interfaces require vtable lookups and processors don't particularly like jumping around all over the place.

  • @adiazwise
    @adiazwise Год назад

    It’s great to see different point of view; we can learn more on this long pathway.
    I will take both opinions.

  • @moke_codes
    @moke_codes Год назад +2

    I wonder when people started adding indirection and abstractions only for the "what if" reasons. How difficult would it be to change every handler because you decide to use Dapper instead of EF Core, in comparison to creating another persistence project that does this and changing every existing query?

  • @enterprise_programmers
    @enterprise_programmers Год назад

    Hi, I'm 16 years old and I love microservices and ddd and I've been on some enterprise projects i really liked your review and you made some excellent points, I recorded some videos about microservices and architecture and I'll be happy if you review them and gave me your thoughts about it and say witch part I'm right or wrong ❤️

  • @leopoldodonnell1979
    @leopoldodonnell1979 Год назад

    Patterns can be awesome, but the practice of turning helpful patterns into boilerplate often ruins the utility of code; even the smallest task requires a survey of tens of files.
    If it is simple, keep it simple.
    I love CQRS because it really enables focus on what’s important.
    I’d also note that no style, whether it is CQRS or Clean, is going to save you from over coupling and low cohesion

  • @jedielson31
    @jedielson31 Год назад

    What do you think of moving query handlers to Persistence layer?

    • @CodeOpinion
      @CodeOpinion  Год назад

      I don't think of layers in terms of clean architecture. I don't think of a "persistence layer". I think of how I handle persistence given a use case or a set of features. That could be in a transaction script if its CRUD, or maybe i have a repository if i'm using an aggregate. It depends on the context. As mentioned at the end of the video, a large factor is how much coupling is involved to the underlying data.

  • @dariuszlenartowicz
    @dariuszlenartowicz Год назад

    Separated purpose WriteSide - ReadSide is not that hard. What about specyfic queries from Write-Side to accomplish use-case?

  • @spicynoodle7419
    @spicynoodle7419 Год назад +2

    Wow, this guy managed to take a single, concise query and spread it around the codebase like he's making a PB&J sandwich.
    This video really felt like reading the *EnterpriseQualityCoding/FizzBuzzEnterpriseEdition*

    • @spicynoodle7419
      @spicynoodle7419 Год назад

      The problem with this video is that he is creating a million classes for a million queries. Creating an abstraction over a single immutable constant. This is wrong. At work I use Eloquent ORM, it has one interface with different DB drivers and that is a way better architecture.

  • @pispis3617
    @pispis3617 Год назад

    "Calling everething as service killing me"
    YES YES YES YES!!!!!!!!!!!!!!

  • @emanuelrodriguez3155
    @emanuelrodriguez3155 Год назад

    Thx for this video derek, it's a good point of view that some future engineers can have when you are learning. I would like to see more of this content.

  • @ahokai
    @ahokai Год назад

    Is using an interface extensible (everywhere) a common practice in C#? If no other implementations are needed for a class, I don't really see why you want to use an interface for it. Would like to hear others' comments.

  • @crazyfox55
    @crazyfox55 Год назад

    I think the example is lacking complexity. If the domain was more complex with the query handler needing to collect or modify the response based off some domain specific rules. Then creating some services to allow the query handler to become persistence ignorant would allow for a more focused query handler.

    • @CodeOpinion
      @CodeOpinion  Год назад

      Question: If you're underlying data model was used in a dozen places, would you want to want the query handler to be persistent ignorant?

    • @crazyfox55
      @crazyfox55 Год назад

      @CodeOpinion if the goal is DRY then maybe that is a good reason too.

  • @ToadieBog
    @ToadieBog Год назад +1

    Awesome video. Ok but seriously...there at the end...he has snake eyes, I swear. It was kind of mesmerizing.

  •  Год назад

    What I've seen everywhere
    1. Super convoluted - need a big team with specialists to maintain it
    2. Build on top of bad technologies, thinking vanilla tech, eg. EcmaScript2020, is obsolete
    3. Use the wrong methodologies (Agi$e)
    4. Use the wrong architecture and the wrong paradigm (abstract and interface rules!)
    5. Designed by incompetent with big mouth (cronyism) - using fast talk technique to seduce the moron leader in place
    6. Let users design the features they want - letting developer build a Frankenstein monster

  • @MrFreddao
    @MrFreddao Год назад

    "Calling everything as service kills me" HAHAHA Indeed, this days you can name everything "service" and nobody will claim.

  • @CesarDemi81
    @CesarDemi81 Год назад

    Clean Architecture is one of the biggest trojan horses. And there are people trying to justify it by any means. Incredible!

  • @mohammadtoficmohammad3594
    @mohammadtoficmohammad3594 Год назад +1

    Thank you both , i personally don't like commenting on others content, you could just place your opinion without personalization, coding is like writing a poet its not science, there are many ways to solve same problem, interface for persistent layer is a requirement for clean architecture , domain should not depend on persistent layer

    • @mihais2911
      @mihais2911 Год назад

      As long as you're not being disrespectful, why not commenting on others content? Derek has a lot of experience hence a lot to say. I love what he did here. I bet Milan and many others learned a lot from this video. It's a toxic mentality that commenting on others people is a bad thing. It's my pet peeve.

    • @mohammadtoficmohammad3594
      @mohammadtoficmohammad3594 Год назад +1

      @@mihais2911 judge me when you are perfect, and i am sure no one is perfect

    • @mihais2911
      @mihais2911 Год назад +1

      @@mohammadtoficmohammad3594 But you just judged Derek because he commented on Milan's work.

  • @mabdullahsari
    @mabdullahsari Год назад

    06:50 That's you explaining what a Poltergeist / Gypsy Wagon is. 👍

  • @toopkarcher
    @toopkarcher Год назад

    It's like we forgot that our job is to bring value to people, not marvel at some fancy abstractions or design patterns.

  • @MohamedCherifBOUCHELAGHEMdz23
    @MohamedCherifBOUCHELAGHEMdz23 Год назад

    Can you make a video how to design testable code?
    Code that allows to write tests easily and those tests can run really quick in milliseconds?

  • @furious2563
    @furious2563 Год назад +1

    Curious about your pet peeve, what do you call things that others would call a 'service'? In the context of this 'orderService', I think I would have called it 'orderStorage'.

    • @CodeOpinion
      @CodeOpinion  Год назад +4

      I wouldn't create it in the first place so I wouldn't need to think about naming it. But to answer somewhat, let's say I had to make a call for currency exchange. I'd call it CurrencyExchange.

    • @CoreCommander2
      @CoreCommander2 Год назад +4

      Names should convey meaning. Calling something ProductService, for example, doesn't tell me anything about what the service does. I know it works with products and that's about it. ProductCatalogService is a better name - now I know that the service is responsible for listing and displaying products, but I could figure that out if it was just named ProductCatalog. The Service part is superfluous.

    • @wertrager
      @wertrager Год назад

      @@CoreCommander2 But would you put it ina a Services folder?

    • @CoreCommander2
      @CoreCommander2 Год назад +3

      @@wertrager no, definitely not. I'd put it in a folder for the feature or bounded context. Namespacing by technical concerns is an anti-pattern IMO and it drives me nuts when I see it. Don't even get me started on project-per-layer...

    • @Galakyllz
      @Galakyllz Год назад

      @@CoreCommander2 I've come to find that "services" provide the one-liners for "handlers" (making testing easier), "managers" provide useful functions for "services" (aggregating data), "repositories" provide data access for "managers" (connecting directly to storage). So, for me, when I see a "service" I know exactly what scope of the problem it's solving.

  • @nickolaki
    @nickolaki Год назад

    I would love to hear why you don't approve of sending back your data schema object. I sometimes find myself mapping DTO on domain entities and they have the same exact properties as the entity itself.

    • @juniorzucareli
      @juniorzucareli Год назад

      Well, they change for different reasons.
      There really are times when your dto has many properties of your domain model.. however, there will be times when you don't want to return all the data that is in your domain model, some sensitive data..
      But in general, I don't like to put rules, it depends a lot on the type of application you are developing.

  • @13odman
    @13odman Год назад

    Great video. I'm struggling with the same indirection thoughts.

  • @svenglansberg
    @svenglansberg Год назад +2

    All credit to @MilanJovanovicTech for agreeing to this. Milan serves as an excellent role model for developers of all ages, showing that we are all both mentors and mentees throughout our software development careers. He demonstrates great courage and vulnerability. Something for everyone to aspire to.

  • @robotrabbit5817
    @robotrabbit5817 Год назад

    This style of video is great, but also your traditional way of doing it is.
    This style allows us viewers to get their thoughts verified heir on approaches out there on RUclips that do not really discuss the approach. The video you reviewed felt like a how to video but only your commentary added the discussion.

  • @Masteroxify
    @Masteroxify Год назад +2

    I am really DDD guy. So i would make handler which inject repository, validator and mapper. Firstly we validate a request then repository return desired entities and request handler maps it to response.
    Additionally request handler should handle all possible exception imo (or we can leave it in Filters, i dont know)
    NotFoundException is good but in Write operation. In Read operation it is completly expected to not found values.

  • @adrian_franczak
    @adrian_franczak Год назад

    Great video! I hope this will have more views than original one.

  • @SergijKoscejev
    @SergijKoscejev Год назад

    I just use odata for query separations. I cant imagine how much time i've saved for a single-developer side job

  • @wallyhighsmith9005
    @wallyhighsmith9005 Год назад

    You guys are on to something …. This content is very valuable

  • @gordonfreimann
    @gordonfreimann Год назад

    Noone talks about how difficult it is to introduce transaction logic into this madness

    • @carlosbaldwin6335
      @carlosbaldwin6335 Год назад

      it's really easy. Just call services and repositories inside handler's and that's it. Use strategy instead.

  • @KieranFoot
    @KieranFoot Год назад

    I'm so confused by the video you're commenting on. Mediation allows for clearly defined command/query handlers that server no other purpose. If he wanted to use services for his actions, then why use mediation at all? All he is doing is making the code base more complex with no added value.

  • @pyce.
    @pyce. Год назад

    I think the concept was "with" Milan, he just didn't manage to demonstrate it in the best way.
    I do believe clean architecture is the best approach in most cases and I think Amichai demonstrates the gist of it terrifically: ruclips.net/video/1OLSE6tX71Y/видео.htmlsi=WlEuQ-dLh6I98-Jx

  • @GeorgeTsiros
    @GeorgeTsiros Год назад +2

    How do i put it gently...
    People so young should not be giving advice.
    Was that too up-front?
    He looks too young to be giving advice on how to code tetris.
    I'm 43, I've been programming as a hobbyist, a scientist or a professional for some 30 years now. I _still_ feel wholly inadequate to give any advice beyond "make sure you fully understand what you write" and "never assume".
    Am I missing something? Please?

  • @mux____
    @mux____ Год назад

    This was fire- love this style of video

  • @Musician4231
    @Musician4231 Год назад

    Incredible mashup and discussion!!

  • @eminovperviz
    @eminovperviz Год назад

    Dear Martin,
    I like your videos and Milan have got nice video contents as well.
    What would be your approach in this scenario ? And, do you have "Clean Architecture" or "Some Architecture" example, skeleton or something like this ?
    Maybe you have already shared a video about this concept that i have missed ))
    If you annswer, i will be glad. Thanks in advance 👍

  • @E_G_
    @E_G_ Год назад

    Great video,, two guy I love to watch. Thank you all to creating good contents.

  • @DamageSoftware
    @DamageSoftware Год назад

    Yeah I stopped witching the moment this guy created a service. The whole point of vertical slice architect is to get away from those GOD services.

  • @atechdude
    @atechdude Год назад

    What a huge mess IMO. Too many abstractions and absolutely no gain except pissing off Junior Devs LOL. I am not saying expose your Entities to your UI / Controllers, Razor Pages etc... So some level of abstraction is needed. You are still going to be in mapping hell when it comes to your presentation layer but that is somewhat expected. Get a tool like Mapperly for that. Great video as always man. I was cracking up on your opinion of naming everything IInsertResponsibiltyHereService hahaha.

    • @CodeOpinion
      @CodeOpinion  Год назад

      IEveryingIsAServiceApparentlyAndNeedsAnInterface_EndSarcasm

    • @atechdude
      @atechdude Год назад

      HAHAH

  • @Fred-yq3fs
    @Fred-yq3fs Год назад

    Indirection is bad. I pondered the same questions and came to the conclusion the query side can use all the EF goodness as is. Persistence agnostic / clean architecture should not become a cult, especially if you want to use CQRS. Anyway, how often do you change your persistence level? Do it per query then. If you have a generic GetOrder service + a specific mapper then you're presumably sharing code between handlers, and we know code sharing is bad: SRP principle, right? It's better to be specific, stick to the problem at hand. Besides, don't throw exceptions for flow control, and pass down those cancellation tokens whereever you can: spare resources!

  • @outwithrealitytoo
    @outwithrealitytoo 2 месяца назад

    I want to add an API DELETE end-point to a 10 year old codebase - that should be a change to a config file and writing a new function/method/procedure. 15 files later and 4 layers of abstraction down I'm doubting my sanity and my ability.
    I'd rather have 10 repeated lines at the top of the method for cross cutting concerns, than 10 lines in 10 different methods in 10 different files referencing 10 different entities in 10 different hierarchies. One is straightforward and visible and the other abstruse, error prone and confusing.
    "1 usage 1 implementation" and one line - what a fantastic abstraction; three annotations, an implicit call the super() and data manipulation via on ORM; and I have no idea what magic it is up to.
    Maybe you have a lot of complicated methods with data from numerous different data sources - by all means code those with layers of abstraction. But how many simple calls turn into "Enterprise Fizz-Buzz" to unnecessarily match the Enterprise Architectue.
    Yeh, pseudo-PTSD flashbacks from the last two sprints.
    The original video was great - a clear explanation of how it goes together; and the reaction video was quite measured. I tihnk both videos are very useful. People have to pick up 10 year old projects with this sort of structure, and people starting out need to be put off the idea that this is the pinnacle of good taste in design.

  • @paulovictor9439
    @paulovictor9439 Год назад +4

    Overengineering is unfortenately what a lot of "Senior Developers" love... Always throwing the excuse that the more layers the better because the project will "grow"... Can't stand that anymore

  • @keithnicholas
    @keithnicholas Год назад

    always makes me cringe when people abstract away from the database/datastore, which are often highly optimized data container/manipulators that often are treated as dumb storage services. Backends need to be as thin as practical!

  • @tauraamui
    @tauraamui Год назад

    I agree please stop calling your provider abstractions a service