A service layer is a common approach in enterprise applications to encapsulage application specific use cases. So when I talk about service layers I mean the service layer pattern as described in the “pattern of enterprise application architecture” book by Martin Fowler.
While the pattern describes the responsibility of a service layer from a high level architectural perspective, this blog is about to take a closer look at the most used service layer designs.
Definition of a service
A service provides methods for it’s clients. These methods are typically on a coarse-grained level and normally encapsulate use cases. Therefore the service layer methods are often the transaction boundary of an application. Every service method receives a request and gives a response to the client. The request is made by calling the service method with parameters while the response is the return value of a service method. Whereas exceptions are a special type of response that signals an exceptional state that the client might be able to recover from. A service api is designed by specifying the serivce methods and their request and response types. These request and response types are the serivce methods pre- and post-conditions.
The most used designs differ in how request and response types are specified.
One design is to use a dedicated request and response type for each service method and the other design is to share request and response types between service methods.
Both strategies have their pros and cons as we will see in the next sections.
Sharing request and response types between service methods
First I would like to take a look at a design that is widely used.
In a lot of application you find the service method design of shared request and response types.
The next example shows how such a service api might look like.
public interface OrderService { public OrderTO placeOrder(OrderTO orderToPlace); public OrderTO getOrderDetails(OrderTO orderTOGet); }
The benefit of that design is that you will need only a few request and response types to define all your service methods.
The price you have to pay is that you don’t know clearly in which state a request or response object is when it is returned by the service method or in which state it must be to call the service method. But the state of the request and response objects is important, because the state is the service methods pre- and post-condition. Therefore the request and response type’s state is part of the service method’s api. This part of the service api can not be expressed in the request and response types design, because one type fits all service methods and therefore it must have all properties for all situations. Even those that are not necessary. This part of the service method’s api can only be described in the javadoc of each service method.
Take a look at the method placeOrder(). It takes an OrderTO as its argument and returns an OrderTO. The OrderTO passed into the service method specifies which items a customer wants to order while the OrderTO returned specifies the items that are ordered. E.g. after an order is placed it has a order number to identify the order and maybe also a placed date. But how do we handle both states in one type? We can only do this by adding all attributes that are needed in any state and provide accessor methods to modify them. Most projects do this by adding getter and setter methods to the request and response types for each attribute.
The OrderTO might be implemented as:
public class OrderTO { private List<ItemTO> orderItems; private int customerNumber; private Date placedDate; private Integer orderNumber; public OrderTO(){; } public void setItems(List<ItemTO> items){ this.items = items; } public List<ItemTO> getItems(){ return orderItems; } public void setCustomerNumber(int customerNumber){ this.customerNumber = customerNumber; } public int getCustomerNumber(){ return customerNumber; } public Date getPlacedDate(){ return placedDate; } public void setPlacedDate(Date placedDate){ this.placedDate = placedDate; } public Integer getOrderNumber(){ return orderNumber; } public void setOrderNumber(Integer orderNumber){ this.orderNumber = orderNumber; } }
The second method getOrderDetails() returns an order object that has much more information than the order object passed to that method, but in which state must the OrderTO be that is passed to the service method?
The object passed into that method must only contain the order number, because it’s all that the service method needs to find the order and it’s details. So the client must create an OrderTO and set only the order number to call getOrderDetails().
The problem of using one request and/or response type for different service methods is that the objects can be in any state. A client must know which properites to set before it can pass the TO to the service method and it must know in which properties will be set when the TO is returned. This also means that the service method must validate the request object before it continues with the service, because the service must ensure that all required properties are set (fulfills the service’s pre-condition). The service method must also ensure that the response object is in the state that the client expect it to be.
So if we think about the service methods defined above, a valid implementation of the placeOrder service method can be:
public class OrderServiceImpl implements OrderService { public OrderTO placeOrder(OrderTO orderToPlace){ if(orderToPlace.getPlacedDate() != null || orderToPlace.getOrderNumber() != null){ throw new IllegalArgumentException("order " + orderToPlace.getOrderNumber() + " has already been placed on " + orderToPlace.getPlacedDate()); } List<ItemTO> items = orderTOPlace.getItems(); if(items == null || items.isEmpty()){ throw new IllegalArgumentException("order must have at least one item"); } ... // do business logic int orderNumber = ...; Date placedDate = ...; OrderTO placedOrder = new OrderTO(); ... // copy all properties placedOrder.setOrderNumber(orderNumber); placedOrder.setPlacedDate(placedDate); return placedOrder; } }
Of course this design is used in a lot of enterprise applications, but there are pitfalls. These pitfalls are based on the fact that the OrderTO does not respect the single responsibility principle. The responsibility of the request objects is to hold all data that is needed by the service method to fulfill it’s service – nothing less and nothing more. If an object has multiple responsibilities it can be in a lot more states. The consequence is an hard and error-prone object state handling that makes the source code unreadable and confuses clients and service implementors.
Dedicated request and response types per service method
There is also another approach of service api design. This approach uses one request and response type per service method.
public interface OrderService { public OrderPlacedTO placeOrder(PlaceOrderTO placeOrder); public OrderDetailsTO getOrderDetails(OrderDetailsRequest orderDetailsRequest); }
This service api design uses a dedicated request and response type per service method. The difference to the shared request and reponse type design is that the request types hold only the data that is needed to execute the service method. The response types contain only the data that the client needs. This design helps the client to understand which properties it has to provide.
public class PlaceOrderTO { private List<ItemTO> orderItems; private int customerNumber; public PlaceOrderTO(List<ItemTO> orderItems, int customerNumber){ if(orderItems == null){ throw new IllegalArgumentException("orderItems must not be null"); } if(int customerNumber < 0){ throw new IllegalArgumentException("customerNumber is invalid. Must be 0 or greater"); } ... // additional validation this.orderItems = orderItems; this.customerNumber = customerNumber; } public List<ItemTO> getItems(){ return orderItems; } public int getCustomerNumber(){ return customerNumber; } }
In the example above I also use the constructor of the PlaceOrderTO to ensure that the required properties are set, because the constructor will prevent the creation of an invalid object. Optional data can still be provided via setter methods. This approach is useful when have only a few required parameters. Otherwise the constructor parameters will get very long and confusing too. In such situations it might be better to just have a default constructor and getter/setter methods for the properties. This approach is still better for the client because the PlaceOrderTO still has no properties that the serivce doesn’t need. But the client might still forget to set a required property and therefore the serivce must validate and can not be sure that only valid objects exist. So if you can use the constructor as the validator and do not allow invalid objects to be constructed.
Another advantage of the validating constructor is that the validation logic is put at the same place as the data that it validates. This increases data cohesion and decreases the risk to forget to change the validation logic in case of data change.
If we use dedicated requrest and response types for each service method changes to these types do not affect other service methods.
The validating constructor makes the service method implementation much easier, because the validation of the request type is already done if a request object exists:
public class OrderServiceImpl implements OrderService { public OrderPlacedTO placeOrder(PlaceOrderTO orderToPlace) { if(orderToPlace == null){ throw new IllegalArgumentException("orderToPlace must not be null"); } ... // do business logic Date placedDate ...; int orderNumber; OrderPlacedTO orderPlaced = new OrderPlacedTO(orderNumber, placedDate, orderToPlace); return orderPlaced; } } } public class OrderPlacedTO { private PlaceOrderTO orderThatWasPlaced; private Date placedDate; private int orderNumber; OrderPlacedTO(PlaceOrderTO orderThatWasPlaced, int orderNumber, Date placedDate){ this.orderThatWasPlaced = orderThatWasPlaced; this.placedDate = placedDate; this.orderNumber = orderNumber; } public PlaceOrderTO getOrderThatWasPlaced(){ return orderThatWasPlaced ; } public Date getPlacedDate(){ return placedDate; } public int getOrderNumber(){ return orderNumber; } }
A client knows which TO a service method needs and the dedicated TO tells the client which data it must provide.
You might have recognized that the OrderPlacedTO constructor is package scope. I have done this because the OrderPlacedTO is the return type. The client must not create instances of that type and taking the possibility to do it helps the client to understand how that object is used.
The service method on the other hand can be sure that if a OrderPlacedTO exists it has been created by the service layer.
Comparison of both design strategies
Now that we looked at both strategies I would like to compare the advantages and disatvantages of both.
The next table summerizes the advantages (+) and disadvantages (–).
Advantages and disadvantages comparison
shared types | dedicated types |
---|---|
+ small number of request and response types | + fail-safe and easy changes of methods, because a change does not affect another method |
+ easy to add new service methods, because types are re-used |
+ one responsibility for one type makes clear what the type is about |
– Implementing the client code is error-prone, because the client must ensure that it brings the request object to the expected state. |
+ Only define the properties a service need. A client can easily see which data it must provide. |
– complex request and response object state handling |
+ unit testing of the service method’s pre and post-conditions can be done if the constructor validation is used. |
– changing the request and/or response objects is error-pron, because they are used in multiple service methods. All dependent service methods must be retested if one is changed. |
– large number of request and response types |
Design recomendation
I prefer the dedicated request and response types. The reason’s are explained above in detail and I would like to list them here in short:
- The service method’s api is easier to understand, because only properties are defined that the service needs.
- Fail-safe change of service methods, because the request and response objects do not inferr each other.
- Constructor validation can help to ensure that only valid request/response objects exists, This makes the client and serivce code easier, because client and service can count on these constraints.
How to handle the disadvantages of dedicated request and response types
- large number of types
The large number or request and response types can not be really handled, but you can define them as interfaces and not as classes. When you define them as interfaces you can implement multiple types by one class and benifit from re-use. But keep in mind that this also means that you have dependencies between service methods “under the surface”. So you can not benifit anymore from “fail-safe changing of service methods”, because the interface’s implementations are used accross the service method implementations. But you can still benifit from that design, because the client only sees the interfaces and therefore it’s clearer to the client how to use the service api. The side-effect is that you have to introduce a factory to allow the client to create request objects, because it is only an interface and the client should not know the implementation.
Hi! Thanks for the great comparison!
This article was written 6 years ago, would like to what do you think about dedicated objects design now, in the era of distributed services which cannot communicate other than by DTOs?
The second question is that a large number of types leads to lots of partially duplicated conversion logic. Is it a good practice to use some model mapping tool in case of dedicated objects approach? and won’t it be hard to adjust such a tool when we have a few domain and many transport objects?
Hi Yury,
I guess you are talking about microservices. A microservice also has more than one service method and I would create dedicated DTOs for each service method.
To your second question…
A large number of types can lead to a lot of duplicated code. But you can use design patterns and extract duplicated logic for re-use. E.g. if there is a bank account and the bank account information must be mapped to a lot of DTO types you might introduce an interface like BankAccountAttributeSource that you pass into the DTO. The DTO simply calls that interface’s methods and does not know where the values come from or how they are converted.
public interface BankAccountAttributeSource {
public String getIBAN();
}
If some DTOs need a sligthy different representation of some attribute, they could pass in a parameter. E.g.
public interface BankAccountAttributeSource {
public enum IBANFormat {
EN_BLOC, // E.g. DE1234454234354645
IN_BLOCKS // E.g. DE12 3445 4234 3546 45
}
public String getIBAN(IBANFormat);
}
The DTO simply uses that interface in it’s constructor
public class SomeDTO {
public SomeDTO(BankAccountAttributeSource bankAccountAttributes){
this.iban = bankAccountAttributes.getIBAN(IN_BLOCKS);
}
}
I hope I found a simple case that shows you how mapping and transformation logic can be used accross many DTOs.
Greetings
René