Generic controllers and services in Spring Boot Java

Ever got that feeling, that you are writing the same thing over and over again? Logic behind your controllers and services are basically the same? That's a sign you can create more general approach with generics. Pros? You can keep your sanity and you keep your code DRY.


In this tutorial we will create generic controller and service that implements CRUD operations for all entities you want. Of course you can add more complex tasks in services, but here we will keep it simple.

Generic entity

First, we will create interface that will represent our entities in all other classes and expose crucial methods:


public interface GenericEntity<T> {

    // update current instance with provided data
    void update(T source);

    Long getId();

    // based on current data create new instance with new id
    T createNewInstance();

}


Generic repository

And here is our generic repository. Important part is NoRepositoryBean annotation, without it everything will fall apart.

@NoRepositoryBean
public interface GenericRepository<T extends GenericEntity<T>> extends JpaRepository<T, Long> {
}

Generic service

This service implements core logic of our app. To function properly it uses generic repository we've created earlier and methods from GenericEntity interface. W are also throwing NotFound exception which we should handle somehow. Preferably with ExceptionHandler, which i have covered in previous tutorial. It is important to make this class abstract and to NOT annotate it with Service. We will create beans manually later.


public abstract class GenericService<T extends GenericEntity<T>> {

    private final GenericRepository<T> repository;

    public GenericService(GenericRepository<T> repository) {
        this.repository = repository;
    }

    public Page<T> getPage(Pageable pageable){
        return repository.findAll(pageable);
    }

    public T get(Long id){
        return repository.findById(id).orElseThrow(
                () -> new NotFound(id)
        );
    }

    @Transactional
    public T update(T updated){
        T dbDomain = get(updated.getId());
        dbDomain.update(updated);

        return repository.save(dbDomain);
    }

    @Transactional
    public T create(T newDomain){
        T dbDomain = newDomain.createNewInstance();
        return repository.save(dbDomain);
    }

    @Transactional
    public void delete(Long id){
        //check if object with this id exists
        get(id);
        repository.deleteById(id);
    }
}

Generic controller

And our final generic class. Again, nothing complex here. Make this class abstract and do NOT annotate it with Controller or RestController. The most mportant part is constructor. It takes GenericRepository and creates proper GenericService on the fly. We can do that because our services will be used only here.

public abstract class GenericController<T extends GenericEntity<T>> {

    private final GenericService<T> service;

    public GenericController(GenericRepository<T> repository) {
        this.service = new GenericService<T>(repository) {};
    }

    @GetMapping("")
    public ResponseEntity<Page<T>> getPage(Pageable pageable){
        return ResponseEntity.ok(service.getPage(pageable));
    }

    @GetMapping("/{id}")
    public ResponseEntity<T> getOne(@PathVariable Long id){
        return ResponseEntity.ok(service.get(id));
    }

    @PutMapping("")
    public ResponseEntity<T> update(@RequestBody T updated){
        return ResponseEntity.ok(service.update(updated));
    }

    @PostMapping("")
    public ResponseEntity<T> create(@RequestBody T created){
        return ResponseEntity.ok(service.create(created));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> delete(@PathVariable Long id){
        service.delete(id);
        return ResponseEntity.ok("Ok");
    }
}

Usage

Ok, we have this fancy classes, but how to use them?

First, we create our Entity. Here we will use book as an example. All we have to do is to implement GenericEntity methods. Here we are using Data annotation from Lombok to generate getters, setters and other functions.


@Data
@Entity
public class Book implements Serializable, GenericEntity<Book> {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String title;

    private String author;

    @Lob
    private String description;

    private Date releaseDate;

    @Override
    public Long getId(){
        return this.id;
    }

    @Override
    public void update(Book source) {
        this.title = source.getTitle();
        this.author =source.getAuthor();
        this.description = source.getDescription();
        this.releaseDate = source.getReleaseDate();
    }

    @Override
    public Book createNewInstance() {
        Book newInstance = new Book();
        newInstance.update(this);

        return newInstance;
    }
}

Then we will have to create repository bean:


public interface BookRepository extends GenericRepository<Book> {
}

And finally, our book controller:


@RestController
@RequestMapping("/api/book")
public class BookController extends GenericController<Book> {

    public BookController(BookRepository bookRepository) {
        super(bookRepository);
    }
}

We annotate it with RestController and RequestMapping with mapping we want. And that's it! You can create as many entities as you want!