API Design for Microservices
API design plays a great role in building applications. If you don’t design it well, you will have to face cons in future. Microservices architecture allows a big application to divide into many small, autonomous, loosely coupled services. Microservice works as a pluggable component which we can modify anytime without affecting other services in microservice architecture. While working on microservices architecture, we write many services. If we don’t define it well, it creates a lot of problems for other consumer services/API user. If you don’t design proper response, consumer services won’t be knowing whats going on behind the service call. We always write at least 3 layers for any given API ex. Controller, service, dao. We should always try to get a clear separation in terms of responsibility here.
1. Endpoints: There are a lot of discussions around. But we would see recommend practices for this. Let’s start with few examples
There are considerable problems with this practice. Let’s say you are writing one more API for a customer so you would end up writing many similar endpoints using given practice. So at the end, your application will have may endpoints. It would be difficult to maintain these. You would end up designing multiple endpoints for the same resource ex. employee, customer etc. which is not required at all. API endpoints should not contain any verbs, actions. We should have only noun(resource with plural form) in the endpoint. We don’t need to have action in endpoint, there are various https methods available(GET, POST, DELETE, PUT) which works for you here. Now if we follow this practice, we should be having endpoints like as follows:
2. Exception Handling: This is where we need to be very careful as it gives a lot of information to the consumer about your API when it is not working as expected. We should always try to come up with minimally required status codes, messages while working on writing new API. Let’s assume we are writing an API which process payment for given customer order
VALIDATION_ERROR when given input is not correct ex, few fields might be blank etc.
ORDER_NOT_FOUND when don’t find the order with given info.
ORDER_CANCELLED when the order got cancelled.
REFUND_FAILED when refund already processed.
INVALID_AMOUND invalid amount
If you see here it makes consumer life easy, if we design well status codes, messages around our API instead of just using TECHNICAL_ERROR which is very abstract. Always try to come up with minimal use cases for your API. If we use TECNICAL_ERROR for each case, a consumer will have difficulty in understanding the output of our API when something goes wrong. The consumer will have to check logs to get it right which is not recommended. Consumer need not check logs for these basics use cases if we design our API well.
Throw a custom exception(create a custom exception which takes a message, status code as input) from service based on these use cases ex. an invalid amount, input validation, refund failed etc, and handle it in the controller.
Create a final API response by passing exception information to response utility method which takes status coed, a message as input, and give a final response.
Avoid creating a final response at service as API final response contains success, error code, message which should not be done at the service layer. There are instances where we do call different services from one service, and we don’t expect a response in this kind of formate. It breaks the principle of ‘“clear separation between layers”. It is not service’s responsibility to get final API response. it is something which should be done at the controller layer.
3. Controller: Controller is the place where you get request first, We should always have dto classes designed for the controller. Never mix your domain classes(entity/document ) with a controller as we always expect these two to behave in a different manner. Let take an example
Domain class(entity/document) can have many fields, but we might not want to send all to the consumer. Have dto classes with required fields.
There are instances where we need to compare our model classes. Let say while updating any domain object you want to record it in history with changed fields. When you think about comparing two POJO objects, you care about all the fields but domain object can be compared by just using ids. So these classes are not just for passing domain object information from one layer to another layer, there are differences in term of behaviour.
The controller should not have any kind of business logic.
Building a final response at the controller is recommended as this is the place where we should be building final response with given status code, message. Service layer passes status code, a message in the form of custom exception which needs to be handled at the controller layer.
Should not call multiple services from controller instead have new service which invokes multiple services if required. Again we need to be careful in terms of designing it ex. what if the first service throws an exception, gives empty response etc. Try designing proper status code, message around it.
Should not call Dao/repository from a controller as it is not controller responsibility to do so.
4. Service: Service is the place where your API business logic needs to be written.
Always try to divide service methods into small methods. Again if we divide it into private methods there will be issues around designing test cases. You can move these private methods to adapter class, have static methods to achieve the same as it is difficult to cover all possible use cases of private methods from calling test case. Sometimes it requires to have multiple big configurations to test all the scenarios. We can also have these methods as a private package, and keep it in the same service class.
Always do validation on input fields, and throw a custom exception with validation error code, and fields specific error message. If there are multiple fields need to be validated, move it to outside of service. Have validator class to write validation for all the fields. This makes your service methods small, and clean.
Let say there are two domain objects A, B and we have two different repositories defined for these two. There are instances where we need to get other domain objects information. In this case, we should not call B repository methods directly from A service method. Always try to get service class around repository methods even if you do need it in a controller. As there are multiple things which we handle at the service layer, not repository layer ex. transaction, caching etc.
Do not mix two domain object/ repo with the same service method.
When calling external services, or other services if we don’t get the required data, throw a custom exception with required status code, error message.
Don’t do anything which is specific to the repository layer at service. ex. building criteria/query at service layer in case of MongoDB.
Have caching in placed for required service methods.
5. Repository/ Dao: This layer interacts with DB in your application. Database scans every record to get the required result if we don’t design index around entity/document which is highly inefficient. Indexes help in reducing search space. Read this to know more about working of indexes.
We should try understanding find methods first, then come up with proper indexing.
Create index only when it is required as it might make write, delete operation costly. And it would need to update both table/document, and indexed data.
If find needs to get data based on multiple fields, avoid writing multiple methods instead try building a dynamic query based on given fields with single repository method.
6. Unit testing: Unit testing helps you in testing your code independently. if your code methods look very complex or contain many lines of codes it is always difficult to test it. Try breaking into multiple small methods(discussed in the service part). Unit testing helps you a lot when you writing big modules with lots of configuration, and many conditions around it. It saves a lot of time as you don’t need to redeploy your code every time when you make small code changes. You can just verify it through a test case. It also helps you in duplicating application issues, you just need to get configuration around a given issue in your test case, and you can easily fix it.
The API is an interface, which allows many developers to interact with the data. We should always focus on designing great API as it helps a lot in terms of using and understanding it.