I'm quite new to the Spring Framework and one aspect I'm having trouble conceptualizing is the synergy between services when using DTOs.
I'll try to give a simple example. Might not be the most pertinent and certainly not well designed but it illustrates my problem:
@Entity
public class Author {
private int id;
private String name;
}
public class AuthorDto {
private int id;
private String name;
}
@Entity
public class Book {
private int id;
private String title;
private Author author;
}
public class BookDto {
private int id;
private String title;
private String authorName;
}
// AuthorRepository extends CrudRepository
// BookRepository extends CrudRepository
@Service
public class AuthorService {
public AuthorDto getByName(String name) {
Assert.nonNull(name, "name is null");
Author author = this.authorRepository.getByName(name).orElseThrow(() -> new AuthorNotFoundException(name));
return AuthorMapper.toDto(author);
}
}
@Service
public class BookService {
public BookDto register(BookDto book) {
// Check book validity.
Book bookEntity = BookMapper.toEntity(book);
// How do I get the Author information for entity persistence ?
Author author = ???
bookEntity.setAuthor(author);
bookEntity = this.bookRepository.save(bookEntity);
return BookMapper.toDto(bookEntity);
}
}
① One solution would be to directly use the AuthorRepository:
public BookDto register(BookDto book) {
//…
Author author = this.AuthorRepsitory.getByName(book.getAuthor())
.orElseThrow(() -> new AuthorNotFoundException(book.getAuthor()));
//…
}
But then I duplicate code from the AuthorService. I imagine that's fine for simple case like this but for more complicated things, like managing the creation of a child object, this could lead to bugs and missed steps.
② An other solution would be to use the AuthorService:
public BookDto register(BookDto book) {
//…
Author author = AuthorMapper.toEntity(this.AuthorService.getByName(book.getAuthor()));
//…
}
But this would add unecessary steps (entity to DTO to entity) and would detached the author from the underlined entity manager, possibly adding more steps under the hood to fix that.
③ My thought would be to take advantage of the protected
scope to ease service communication:
@Service
public class AuthorServiceImpl implements AuthorService {
protected Author _getByName(String name) {
Assert.nonNull(name, "name is null");
return this.authorRepository.getByName(name)
.orElseThrow(() -> new AuthorNotFoundException(name));
}
public AuthorDto getByName(String name) {
return AuthorMapper.toDto(this._getByName(name));
}
}
@Service
public class BookServiceImpl implements BookService {
public BookDto register(BookDto book) {
// Do check for book validity.
Book bookEntity = BookMapper.toEntity(book);
Author author = this.authorServiceImpl._getByName(book.getAuthor());
bookEntity.setAuthor(author);
bookEntity = this.bookRepository.save(bookEntity);
return BookMapper.toDto(bookEntity);
}
}
So that each service manages its own entities but I don't know if it is a good practice.
I tried to find answers to my question but, as of now, most I've seen would be ① and all concret examples I found, are simple case with only one service or with entities independent from one another.
I don't see a direct question, but I understand that you would like some input on how to set your architecture.
One way that I find useful is to start with a classic three tierd architecture (where the API act as the client) and then simplify if it becomes over engineered.
The client tier, where I have the API, is typically some sort of HTTP API, often REST based returning JSON objects.
Just like your example, the DB layer returns DB objects. Any ORM turns the result of a DB query to an object that maps about 1:1 with the DB layout. You normally annotate this with @repository
in spring.
Furthermore, each service returns domain objects describing the entity managed by that service. In your example, the book service layer would make use of the Book DB layer and the Author DB layer, concatenate the results into a book object. This its the layer with your business logic. (Here comes the first simplification, if books and authors are in the same DB, you often do the join in the DB layer already.) This is annotated @service
in spring.
Finally, the API layer creates the DTOs. Here, the methods annotated @controller
in spring, uses the response from one or several service(s) to create DTOs according to the API spec and returning that on the API. This tier owns the DTO classes and the transformation from domain objects to DTO objects. An example DTO would be your BookDTO that is serialized to a JSON object for a HTTP/REST API, but it could also be a protobuf for gRPC, XML for a SOAP API, combinations there of, etc. The point is that the DTO for the API should beneficially be disconnected from the service.
This separates different concerns within a product. Changing DB layout for performance, or changing business logic as your product evolves, or changing API as you get new type of clients, all these changes should be separate tasks touching separate layers in your application.
Having that said, another simplification is (for me) often to lett the service return the DTO directly. Especially if you are not using Kotlin or libraries like Lombok or Immutables to help out with the dull boilerplate code for managing POJOs.