The following is the story of a UserCredentialsDto
that arrived at one of my endpoints and how it finds its way through all the layers namely
- REST Layer
- Service Layer
- Repository Layer
- Persistence Layer
The example below is very simple and one could argue that anything more complex goes towards over engineering but I know for a fact that things get more complicated over time. I have two major issues for which I do not have a "clean" solution:
- I am mapping DTOs to Models and vice versa in ther Service Layer
- The "business logic" gets somehow mixed up within the Service Layer
I have thought about different solutions but none of them make the current state simpler.
So, once upon a time there was a
public class AppUserCredentialsDto {
// ...
}
which found it's way into the
REST Controller Layer
The first layer is very straight forward. Receiving the DTOs, pass it to the Service Layer and handle exceptions in case there are some.
@RestController
@RequestMapping("/pub")
public class AppUserController {
@Autowired
private AppUserService appUserService;
@RequestMapping(value = "/register", method = RequestMethod.POST)
public AppUserDto register(@RequestBody AppUserCredentialsDto appUserCredentials) {
return this.appUserService.register(appUserCredentials);
}
}
Service Layer
In the Service Layer it gets messier. The DTO gets mapped into a Model (Hibernate managed POJO), some other business logic happens there (hash password) and it's getting passed to the Repository Layer where, in this case, the data gets persisted.
@Service
public class AppUserService {
@Autowired
private AppUserRepository appUserRepository;
@Transactional
public AppUserDto register(AppUserCredentialsDto appUserCredentialsDto) {
String salt = BCrypt.gensalt();
AppUserModel appUserModel = new AppUserModel();
appUserModel.setUsername(appUserCredentialsDto.getUsername());
appUserModel.setPassword(BCrypt.hashpw(appUserCredentialsDto.getPassword(), salt));
appUserModel.setPasswordSalt(salt);
this.appUserRepository.add(appUserModel);
appUserModel = this.appUserRepository.findByUsername(appUserCredentialsDto.getUsername());
return AppUserDto.fromModel(appUserModel);
}
}
Repository Layer
Here, Model objects are received and e.g. persisted of returned based on certain criteria.
@Repository
public class AppUserRepository extends AbstractRepository {
public void add(AppUserModel appUser) {
Session session = this.getCurrentSession();
session.save(appUser);
}
public AppUserModel findByUsername(String username) {
Session session = this.getCurrentSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<AppUserModel> query = cb.createQuery(AppUserModel.class);
Root<AppUserModel> appUserTable = query.from(AppUserModel.class);
CriteriaQuery<AppUserModel> appUserQuery = query.select(appUserTable)
.where(cb.equal(appUserTable.get("username"), username));
AppUserModel appUserModel = session.createQuery(appUserQuery).getSingleResult();
return appUserModel;
}
}
Persistence Layer
There's nothing to say about this layer since it is entirely managed by Hibernate. Let me call these objects Data Access Objects and please correct me if that terminology is wrong because I am not 100% sure if @Entity @Table
objects can actually referred to as DAOs.
@Entity
@Table(name = "app_user")
public class AppUserModel extends AbstractTimestampEntity {
@Id
@GeneratedValue
private Long id;
@Column(nullable=false, unique = true)
private String username;
// ..
}
Although this is just a short example it already shows some of the issue my current design carries with it. As mentioned before, I have no clean solution for the mapping part yet. In this example it's just a few lines but it's getting messier of course if I have larger collections of data. I do not want to end up having multiple nested loops in the Service Layer, and maybe more complex logic, in order to map a DTO into a Model. I know one could start writing dedicated Mapper classes which provide static functions in order to do the job:
public final class AppUserMapper {
private AppUserMapper() {}
public static AppUserDto toDto(AppUserModel appUserModel) {
// ..
}
public static AppUserModel toDto(AppUserDto appUserDto) {
// ..
}
}
but this can also become rather ugly over time.
Current "Solution"
One way I could move this code out of the Service Layer would be to introduce my missing Business Layer. I think my problem here is that I don't know at this point how this layer is supposed to communicate with the others. Imagine a business/domain object like this:
public class AppUser extends DomainObject {
@Autowired
private AppUserRepository appUserRepository;
private AppUserModel appUserModel = new AppUserModel();
public AppUser(AppUserCredentialsDto userCredentialsDto) {
String salt = BCrypt.gensalt();
this.appUserModel.setUsername(userCredentialsDto.getUsername());
this.appUserModel.setPassword(BCrypt.hashpw(userCredentialsDto.getPassword(), salt));
this.appUserModel.setPasswordSalt(salt);
}
public AppUser(String username) {
this.appUserModel = this.appUserRepository.findByUsername(username);
}
public void create() {
this.appUserRepository.add(this.appUserModel);
}
public void update() {
this.appUserRepository.update(this.appUserModel);
}
public void setUsername(String username) {
this.appUserModel.setUsername(username);
this.appUserRepository.update(this.appUserModel);
}
@Override
public AppUserDto toDto(AppUserModel appUserModel) {
AppUserDto appUser = new AppUserDto();
appUser.setUsername(appUserModel.getUsername());
appUser.setActivated(appUserModel.getActivated());
return appUser;
}
}
which would simplify the Service Layer to:
@Service
public class AppUserService {
@Transactional
public AppUserDto register(AppUserCredentialsDto appUserCredentialsDto) {
AppUser appUser = new AppUser(appUserCredentialsDto);
appUser.create();
return appUser.toDto();
}
}
and although this looks nice so far I am not sure whether I am over complicating things more than necessary, not seeing a significant flaw with this design and run straight ahead in massive issues in the long run or whether both of those statements are true.
So, I really hope somebody takes a look at this example and can offer me some advice on how I can improve things.
If something is not entirely clear please let me know and I'm happy to provide further information if it helps.