Salta al contenuto principale

Creare e gestire i servizi in Spring Boot

Profile picture for user luca77king

Nella costruzione di applicazioni solide, scalabili e facili da mantenere con Spring Boot, la corretta suddivisione dei livelli logici rappresenta un principio architetturale fondamentale. Finora abbiamo visto come strutturare l’applicazione, gestire il contesto di Spring, creare bean, definire le entità, configurare l’accesso ai dati e realizzare i repository. Tuttavia, tra la persistenza e il livello di presentazione esiste un elemento intermedio indispensabile: il livello dei servizi. Questo layer è il cuore dell’applicazione, dove vive la logica di business, cioè l’insieme delle regole che trasformano i dati grezzi provenienti dal database in informazioni utili e pronte per essere utilizzate.

L’errore più comune di chi inizia con Spring Boot è scrivere tutta la logica direttamente dentro i controller oppure nei repository. Questo approccio funziona nei progetti piccoli ma diventa rapidamente ingestibile in contesti reali, dove la manutenzione, la riusabilità e la testabilità del codice sono prioritarie. Il livello dei servizi serve esattamente a questo: incapsulare la logica di business e creare un punto di contatto pulito e coerente tra i controller (che si occupano solo di ricevere e rispondere alle richieste) e i repository (che interagiscono direttamente con il database).

Con il livello dei servizi otteniamo numerosi vantaggi:

  • Separazione delle responsabilità: ogni livello fa una cosa e la fa bene.

  • Manutenibilità: modificare la logica non impatta su controller o repository.

  • Riusabilità: la stessa logica può essere richiamata da più controller o persino da altri servizi.

  • Testabilità: è più semplice scrivere test unitari mirati senza dover simulare l’intero stack.

Spring Boot fornisce tutti gli strumenti necessari per implementare i servizi in modo elegante e dichiarativo, in particolare grazie all’annotazione @Service, che consente a Spring di rilevare automaticamente le classi di servizio e gestirle come bean all’interno dello Spring Context.

Creare un servizio in Spring Boot

Un servizio è semplicemente una classe Java annotata con @Service e contenente la logica di business. Generalmente, un servizio richiama uno o più repository per ottenere o modificare i dati e applica le regole necessarie prima di restituire il risultato al controller.

Ecco un esempio di base per comprendere la struttura:

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public List<Product> findAllProducts() {
        return productRepository.findAll();
    }

    public Product findProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Prodotto non trovato"));
    }

    public Product createProduct(Product product) {
        return productRepository.save(product);
    }

    public Product updateProduct(Long id, Product updated) {
        Product existing = findProductById(id);
        existing.setName(updated.getName());
        existing.setPrice(updated.getPrice());
        return productRepository.save(existing);
    }

    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

Questa classe rappresenta un tipico servizio di gestione prodotti. Ogni metodo incapsula un’operazione di business e interagisce con il repository senza che il controller debba preoccuparsi di come i dati vengono ottenuti o modificati.

Chiamare un servizio da un controller

Il controller ora può limitarsi a ricevere le richieste HTTP, delegare tutto al servizio e restituire la risposta. Il risultato è un codice più pulito, leggibile e testabile:

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAllProducts();
    }

    @GetMapping("/{id}")
    public Product getProductById(@PathVariable Long id) {
        return productService.findProductById(id);
    }

    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productService.createProduct(product);
    }

    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product updated) {
        return productService.updateProduct(id, updated);
    }

    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
    }
}

Notiamo subito la differenza: il controller è snello e chiaro, non contiene logica di business ma solo chiamate al servizio, occupandosi esclusivamente della gestione delle richieste e delle risposte HTTP.

Servizi e interfacce: quando e perché usarle

Per migliorare ulteriormente la manutenibilità, è buona pratica definire un’interfaccia per ogni servizio e creare una classe che la implementa. Questo approccio semplifica la scrittura di test, l’iniezione di dipendenze alternative e l’estensione futura delle funzionalità.

public interface ProductService {
    List<Product> findAllProducts();
    Product findProductById(Long id);
    Product createProduct(Product product);
    Product updateProduct(Long id, Product updated);
    void deleteProduct(Long id);
}
@Service
public class ProductServiceImpl implements ProductService {

    private final ProductRepository productRepository;

    public ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public List<Product> findAllProducts() {
        return productRepository.findAll();
    }

    @Override
    public Product findProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Prodotto non trovato"));
    }

    @Override
    public Product createProduct(Product product) {
        return productRepository.save(product);
    }

    @Override
    public Product updateProduct(Long id, Product updated) {
        Product existing = findProductById(id);
        existing.setName(updated.getName());
        existing.setPrice(updated.getPrice());
        return productRepository.save(existing);
    }

    @Override
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

L’uso delle interfacce può sembrare superfluo in progetti piccoli, ma è una best practice che paga nel medio-lungo periodo, specialmente in applicazioni enterprise o architetture a microservizi.

Gestione delle eccezioni nel livello dei servizi

Il servizio è anche il punto ideale per intercettare e gestire eccezioni specifiche di business. Ad esempio, se un prodotto non esiste o se una regola non è rispettata, il servizio può lanciare un’eccezione custom, lasciando al controller il compito di trasformarla in una risposta HTTP.

public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}
@Override
public Product findProductById(Long id) {
    return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException("Prodotto con ID " + id + " non trovato"));
}

Questa separazione rende il codice più leggibile e coerente e facilita l’adozione di strategie globali per la gestione degli errori, come l’uso di @ControllerAdvice.

Conclusione

L’introduzione del livello dei servizi rappresenta una svolta nell’architettura della tua applicazione Spring Boot. Non è solo una questione di ordine o eleganza: è la base per creare software che possano crescere, evolvere e resistere al tempo. Con i servizi al centro della tua applicazione, il codice diventa più chiaro, modulare, testabile e facilmente estendibile. Questo livello agisce come “collante” tra la persistenza dei dati e l’esposizione delle funzionalità, e prepara perfettamente il terreno per il passo successivo: la creazione e gestione delle API REST, dove metteremo a disposizione del mondo esterno tutto ciò che abbiamo costruito finora.