Reading your question I'm not sure you are thinking about microservices in the same way as me.
For example I would never create a validation microservice. In theory, sure I can see it might work, but say you have your AddUser() method on your UserService, its always gona call the validation service for that particular action right?
Separating out the validation logic is all well and good, but if nothing else is ever going to call it I wouldn't expose that code it as a service.
I see microservices as vertical slices if your monolith rather than horizontal layers, which you may already have.
So for me the naming is easy. UserService, OrderService etc perhaps we have a complex case where you can split these further, but the naming will be clear from the logic which suggest the split. ie AdminUserService/UserService
Obviously you may have a case where two service share some internal logic and ot makes sense to split that out into its own service. But again, the naming is clear from the purpose of the code rather than the layer. ie AccountService and OrderService might both call OrderPricingService.
Additionally don't get hung up on the word 'service' a UserRepository is a repository whether you access it via http or in memory.
Also, don't throw away all your OOP objects with thier business logic and make everything procedural unless it makes sense. You can have your services instanciate objects and call thier methods in a high level microservice rather than convert them all to individual OrderBusinessLogicService.ProcessOrder(Order o) services
Exceptions should be treated just like domain models. Each service works with their own domain models and should have their own set of exception models as well. When communicating with external systems, the service should convert external exceptions to its domain exceptions as soon as possible. Basically I'm saying go with solution #2.
Lets consider the communication from service A -> B. Service A should first of all have an interface defined to decouple the business logic from the implementation of requests to B. In your example A is an account service and B is a user service. So let's call the interface UserService
. This interface would have a set of (ideally) compiler-checked exceptions.
interface UserService
def getUser(id): User throws UserNotFoundException, UserServiceException
You should implement HTTP client for service B so that any service that needs to depend on service B imports the common HTTP client. The error responses from requests to service B will be defined in this HTTP client component. That way they're only defined once.
class BHttpClient
def getUserById(id) =
response = http.get("/users/${id}").send
if (response.status == 404) throw new UnknownUserException
else if (response.status == 500) throw new InternalServerException
else return json.parse[User](response.content)
The implementation of UserService
, HttpUserService
will use that HTTP client to communicate with B, should catch HTTP and transport exceptions from the client and wrap them in the appropriate "domain" exception.
class HttpUserService(client: BHttpClient) implements UserService
def getUser(id) =
try {
client.getUserById(id)
} catch {
case e: UnknownUserException => throw new UserNotFoundException(e)
case e: InternalServerException => throw new UserServiceException(e)
}
Cons: Service A will need to catch and expect that Server B is capable of returning a bunch of errors, adding coupling between Service A and B.
Service A will catch errors from the http client in HttpUserService
and wrap them in meaningful errors for service A. The business logic in service A is decoupled from service B through the UserService
interface. HttpUserService
is coupled to BHttpClient
, but decoupled from service B because you can mock service B at the transport level.
Even if you choose to use a different architecture like @Laiv describes in the comments, you'll still want to decouple yourself from message and events you receive by converting the message models and exceptions into domain exceptions in each service. I don't agree with @Laiv, that it's as cut and dry as asynchronous message architecture or you might as well implement a monolith. There are still big gains that can be made by a synchronous, distributed service oriented architecture like you've described. The first and hardest step of getting the right architecture is to decouple the components. By dividing into microservices early, you can more easily adopt an asynchronous approach later if you need it.
Best Answer
There is no good way to do this. Because your data is split across multiple microservices, you need to join this data manually. So there needs to be some piece of code like:
This is going to be tedious and slow, but because you are using microservices you don't have any reasonable choice. (The unreasonable choice would be to access B's data directly, without going through the microservice interface.)
Where this functionality should live depends … possibly, this should be part of the Microservice A itself, i.e. part of the service that deals with configured users.
When data is split like this over multiple microservices this might be a necessary evil, or could be an indication of a deeper problem. Domain-Driven Design has the concept of a bounded context – a self-contained part of the problem domain your software system is trying to solve. One bounded context should not be split across multiple microservices!
You might want to check which bounded context which aspects of a User belongs to. Why is there a need to deal with configured and unconfigured users, and why is that distinct from users in general? It is legitimate if different bounded contexts have different but related concepts of a “User”, but you have to be clear how they relate. Here, it seems like one User entity is effectively shared across different contexts, and therefore causing problems. Possibly, the solution could be to include the configuredness of a user into the main User context (B).
“The schema is fixed” is not necessarily a good reason to avoid doing this, but perhaps cannot be influenced by you. No technological solution will be able to solve organizational dysfunction – especially not microservices. You'll then have to fall back to tediously checking every user, as in the code snippet above.