he divided his solution into 4 projects. Core, Data, Services and WebApi
Just as a hint for finding more documentation on the subject: Core, Data, Services sounds a lot like the Domain, Persistence (or Infrastructure) and Application layers that Clean Architecture promotes.
You can find many articles on the subject. Here's my favorite resource, but many more exist.
After reading the article I decided to apply the very same logic to my project. After doing so, I realised it took me the whole day and I found myself writing a lot and a lot of code.
If having to spend a day writing code is a problem for you, then you're damned if you do and damned if you don't. There's one thing that's very important to remember:
Clean coding takes upfront time and effort, but will save you more time and effort in the long run.
It may suck now, but if you don't do it, maintaining your codebase is going to suck even more in the future. I've seen many projects go into absolute development hell because good practice was thrown out of the window in favor of quick initial results.
It's not that uncommon for some projects to even collapse in on themselves and never being completed, when the bugs are incessant and the technical debt accrues more and more because no one can be bothered to now invest even more effort to clean the dirty code (compared to having kept it clean from the beginning).
which seemed unnecessary and duplicated
"Duplicated" is ambiguous here.
If you mean to say that within a layer there is a lot of copy/pasted code, then you should evaluate your code for reusable features and try to abstract them.
For example, maybe your repositories have some reusable logic (e.g. getting an entity by their ID) that can be abstracted into a base repository.
If you mean to say that your layers all have the same general structure to them; then you're missing the part where each layer has its own distinct role to play:
- The domain layer (Core) houses your business logic
- The persistence/infrastructure layer (Data) handles your database interaction
- The web layer (WebApi) handles the incoming and outgoing web requests
- The application layer (Services) connects all these layers
Initially, your codebase will be an empty shell, and all these empty shells will look the same. But as you start filling them in, you'll notice that each shell gets filled in differently.
For example, in this article he only have two entities. Hence it seemed like a very simple application. I currently have 28 and that's only the beginning!
28 entities is no small application. That's on the cusp of being an enterprise-grade application. Not that this is a hard rule, but as a personal rule of thumb I usually consider any codebase with over 30 entities to definitely warrant a more robust architecture due to the expected size of the codebase.
The more complex your application is (which is heavily correlated with the number of entities), the more overhead and management you're going to need. This is just a fact of life.
You can cut corners now, but it's going to bite you in the end. The more complex your application (and again, the more entities you have), the harder it's going to bite you.
I ended up having 28 separate repository interfaces, 28 separate service interfaces, 28 actual repositories, 28 actual service classes.
You've falling into what I like to call "the Foo trap". You have a
Foo entity with a
FooController and a
FooDTO. That's boilerplating, and more importantly it misses the domain-driven aspect of the architecture you're trying to build.
In DDD, you have to consider aggregates. Keeping it simple (there are plenty of online resources that go in-depth on this): an aggregate is a logical "subdomain" of your domain logic, which usually gets stored as multiple entities.
For example, Let's say you have a video rental service (it's an outdated concept, but a really simple business case to grasp). You may have many different tables, but these can be grouped into three subdomains: customers, videos and rentals. Each subdomain is likely backed by multiple tables:
- Customer subdomain:
- Video subdomain:
- Rental subdomain:
When you say you have 28 entities, 28 repositories and 28 services, what you're telling me is that each and every entity you have is in its own subdomain. That is indicative of underdeveloping your domain architecture and not analyzing the larger business cases that your application is made up of.
What you're doing right now is building a code-shell based on a database model, and modeling each and every layer based on your data model. Don't get me wrong, that can occasionally be the right approach when e.g. your REST API acts merely as a database proxy client with no real business logic inbetween, but then your architecture does not match your business case.
Much more likely, your application has business value outside of its data storage capabilities, which means that your domain/application logic is going to be significantly different from your repository logic.
28 separate repository interfaces, [..] 28 actual repositories
In general, each distinct class is going to be backed by its own interface. That's just how it is.
I completely understand how this intuitively feels as a lot of duplication when you're new to clean coding. We all thought so when we first started doing it. But this is preparation for the future, when you're going to want to swap one class out with another, and you're going to be very happy that you started with the interfaces from the beginning.
Let's say you don't use interfaces here. You build your application, and the
FooRepository is used everywhere it is needed.
Later during development, it turns out that your Foo requirements have changed (or were miscommunicated), and it turns out you need a
ADifferentFooRepository to handle the Foo data storage logic. Now, you have to go back and change all of your code that used to touch
Compare this to when you had created your
IFooRepository interface from the get go. Your
FooRepository handles the data storage logic, but the rest of your codebase references
IFooRepository, not the class itself.
When you now have to implement
ADifferentFooRepository, all you need to do is ensure that it implements
IFooRepository (which it should, if it replaces the old
FooRepository). And all your existing code, which was referencing
IFooRepository does not need to be changed, since your new
ADifferentFooRepository actually implements that interface.
Note that it's easy to now argue "how hard can it be to change a class reference", but that's because any example I can give you here is always very simple, much more simple than your real-life codebase is going to be.
When your codebase gets to a size where you're no longer capable of intimately knowing every class in it and how they all relate to one another, which is very common for enterprise-grade applications, you're going to need to rely on the cleanliness of your coding to help you make changes to the codebase.
Pro tip: Most IDEs have a way of distilling an interface from a class (or vice versa: implementing an existing interface on a class). Just write the class and generate the interface from it using a few button presses (or vice versa: auto generate the interface implementation of a class). This mostly negates the argument that writing the additional interfaces is a cumbersome process, while keeping your code as clean as possible.
However, something inside tells me I've done a lot of unnecessary work
Frame challenge: it currently looks unnecessary, because you haven't yet reached the point where your current (extra) effort is paying back dividends. The good part is at the end.
which either could be simplified
"Simple to write" is not the same as "simple to read/change". Code is read/changed much more than it is written. Therefore, I'd much rather spend some more effort on the initial writing, if it consistently lowers the effort needed to read/change the code in the future.
A small effort now will reap bigger rewards in the future.
Hence, I am here seeking advice if this article is guiding me in the right direction.
There is no one-size-fits-all solution. The article is roughly in the right direction, but without knowing your codebase and business case, it's impossible to say if this is the best possible approach for you.
That being said, clean coding practices are generally advisable to a large proportion of codebases, so it's a good start. But your particular situation could always warrant changing part of the approach to better suit your needs.
That need to adapt will point itself out over time. Don't try to do it perfect the first time. Just build something that works and prepare for needing to change/improve things later on.
Clean coding doesn't tell you how to write things correctly. Clean coding just makes it easier to change something in the future, when it turns out you've either made a mistake or the requirements have changed.
The issues you've currently mentioned are not "issues" in the sense that they are a matter of you doubting (or not understanding) the usefulness of clean coding practices; rather than actual proof that you should do things differently.