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.
Microservices must own the data they are working on.
Your first approach basically suggests a database-microservice. This is not a good idea, and performance is not the only reason why. You are creating a gigantic dependency magnet, and a single point of failure for the entire application.
In many cases, you actually want to store the relevant historic information for business reasons. But let's consider the case where you really do want to work on the latest data. A good example for this might be a Search service.
It is quite likely that the data-storage requirements of your search service are quite different from those of the product catalog - or your other services for that matter. You probably don't need history data, you might not need all data, you may want to store word frequencies, relate it to mentions on your blog or referrals, and so on.
By giving the Search service ownership of the data, you allow the team to choose the optimal approach and technology based on the specific needs of this particular service.
Best Answer
Due to the multitude of formats for localized numbers and dates, my recommendation is to handle that in your UI.
Microservices Should Work With Data
That means you define the one format for every type of data you intend to exchange:
The services themselves are blissfully unaware of the concepts of l10n and i18n.
NOTE: one area you may need to actually take the RFC-3066 language specifier for user entered text if you intend to provide machine translations.
Consistent Data Drives Consistent Results
Since the services exchange data using known data formats there isn't any ambiguity over whether the date 01/02/12 is 1 February 2012, 2 December 2001, or 12 January 2002. It also means that you can actually process the data directly. Dates are stored as Dates, integers are stored as integers, etc. There's no translation that has to happen in the back-end.
Translate in the UI
There are a number of i18n and l10n libraries for JavaScript and other platforms. They all use the standards based identifiers to specify the user's language and locale. Often times they are provided by the browser as well.
The answers to your specific questions can fall out from what the library supports and what it doesn't.
Accept-Language
header.Again, I would look into seeing what can be done in the client, and reserve any special client/server requests to handle user entered data specifically.