I am trying to figure out a good practice for architecture/layering and unit of work.
We use C# to write MVC front-end apps. The normal structure at the moment is:
MVC
Service / Domain Layer
Repository Layer
EF6
MVC would simply call the service, (no logic here). The Service contains all domain logic. The repository deals with data access, using EF6. The main three reasons for having a repository layer on top of EF are:
- Single responsibility (SRP), the service deals with what the business cares about, and the repository deals with getting and saving the data. Using the repository means they are not mingled into the same method.
- Testing makes it easier to mock the repository. (I am aware that you can now mock the dbcontext, in all honesty have not tried this method yet).
- Abstracting out EF from the domain as it doesn’t really care about it, (this is a very weak argument though).
We would have individual services and repositories for different parts of the system, such as :
- CustomerService
- InvoiceService
- OrdersService
Each repository would have its implementation (i.e. no generic repository), allowing us to edit exactly what we want and create queries that return what we want, rather than getting very large objects or making lots of individual calls, (and avoiding having to pass in lots of include statements from the domain).
This structure works well in general. We find out service layer classes sometimes end up very large (end up breaking SRP). Think we should end up splitting them into sub-services. If it was CustomerService, maybe it would become CustomerQueryService and CustomerAdminService. We also find that this structure helps with the repository as some of our queries end up being pretty large transforming the data into the correct format and not pulling out LOTS more data than required.
Where it falls down for me is when you want to use transactions. This could still work if I turned each repository into a unit of work repository (I think). The problem occurs when you need to do something that cuts across two or more services. For example, when creating an order might want to also call the invoice service to create an invoice. But if for some reason the second part failed you would want to roll back the order as well and return a useful error to the client.
I am not sure how to implement this, or if this structure is a bad idea. How would I go about setting up a unit of work pattern that allows for multiple services and repositories (separation of concerns and SRP)?
There is many concepts in your question that don't relate only to the technical part. I've been there trying to solve it technically but this always fails in the long run. What is important is also what a business expert have to say. This is where Domain Driven Design shines if applied correctly. We as developers try to make assumptions about how the business works without really asking them how would they solve theirs problems. This leads us to everything is transactional stuff, 2PC, cripled and monolithical runtime monsters. My first question is: "Why orders have to be transactional with invoice service?". Ask a business expert. "Do you want to loose a 50M order because of the bug in invoicing system?" This seems very wrong to me. Ask business people how they would like to handle this failure cases.
From what I understand business people would always like to take orders even if invoicing is not available. But I lack context of the business you're working in so I cannot give you a really thorough answer. Maybe there are some corner cases that I'm not aware off.
The response to your problem is very complex, giving just a technical solution would be wrong, but I'll try to point you to some resources that might help you going further. If you're designing complex systems, looking to the Domain Driven Design would be a good start as many of such a problems are addressed by it.
- Ask business people how this case should be handled. Very often they would tell you that they want to take orders even if invoicing is not working. What's the probability that invoice doesn't work ? If this is 1% of the time, the failure is not very important and could be handled directly by the business people (calling people, giving away discounts for not happy customers, etc.) there is many ways business people know how to deal with it. At least they don't loose any order. Which is the most important part IMHO.
- Look into Domain Driven Design Bounded Contexts (BC). Bounded Context is a central pattern in Domain-Driven Design. It is the focus of DDD's strategic design section which is all about dealing with large models and teams. To me Ordering Service and Invoicing Service are two different BC. Don't make them transactional unless there is a valid reason you are aware off.
- How to synchronise then two different BC ? You would also need to look into Domain Events and Aggregates. They are produced by an aggregate root, published asynchronously often by a middlewere like event bus and consumed by other aggregates in the same BC or another BC. A great write up on how to design aggregates is by V. Vernon (the link is in the references below). In you scenario it means that you would produce OrderPlaced events in your OrderService BC (I don't like Service in the name bacause it means different things to different people but I try to stick to your example) they should be persisted so if InvoiceService BC is not available or if there is a bug, these events can be consumed after and invoice created in a later time.
- Eventual consistency: This is induced by the point 3. You have to know what the impact is on your system and how to deal with it. In short, this is a consistency model used in distributed computing to achieve high availability that informally guarantees that, if no new updates are made to a given data item, eventually all accesses to that item will return the last updated value. So according to your scenario it would mean, that if InvoiceService crashes or it is not available, the invoices won't be issued stright away when order is placed by at some later point in time when invoice service will be bring online. This is a very important concept because it helps you deal with complex models and you don't loose any order which is important to business people.
- You've touched upon queries and this is generally also a thing that you have to deal with. Separating reads from writes would also be very benefical. It means that you have one model for writes (state changes of your domain) and another one for reads (just denormalized data for different purposes like UX views, etc.) There is an architectural pattern called CQRS but be aware, that it cannot be applied without having a very deep knowledge about your domain. CQRS is also applied to one or several BC but not to the whole system. It really depends. I mention this pattern here as a further investigation and because from theory when you separate reads from writes you're are doing CQRS. This is very technical architectural pattern so you can just put in in your organization without understanding it fully of what you're doing.
This may not address your problem which may be disapointing for you. But I prefer to give you some hints so you can go further, as I've been there and doing everything transactional is generaly a bad way of dealing with thing in a real systems.
References: