How do I organize service, repository and controller layers in Spring?

Typical Spring Layer Organization

A clean Spring application usually separates code into controller, service, repository, and model/entity layers.

com.example.app
├── AppApplication.java
├── controller
│   └── UserController.java
├── service
│   └── UserService.java
├── repository
│   └── UserRepository.java
├── entity
│   └── User.java
└── dto
    ├── CreateUserRequest.java
    └── UserResponse.java

The usual request flow is:

HTTP Request
    ↓
Controller
    ↓
Service
    ↓
Repository
    ↓
Database

1. Controller Layer

The controller handles HTTP requests and responses.

Use:

  • @RestController for JSON APIs
  • @Controller for server-rendered pages such as Thymeleaf/JSP

Controllers should be thin. They should mainly:

  • Accept requests
  • Validate input
  • Call services
  • Return responses

Example:

package com.example.app.controller;

import com.example.app.dto.CreateUserRequest;
import com.example.app.dto.UserResponse;
import com.example.app.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserResponse> findAll() {
        return userService.findAll();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
}

2. Service Layer

The service contains business logic.

Use @Service.

Services should:

  • Implement business rules
  • Coordinate multiple repositories
  • Handle transactions
  • Convert between entities and DTOs if your app is small or medium-sized

Example:

package com.example.app.service;

import com.example.app.dto.CreateUserRequest;
import com.example.app.dto.UserResponse;
import com.example.app.entity.User;
import com.example.app.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public List<UserResponse> findAll() {
        return userRepository.findAll()
                .stream()
                .map(user -> new UserResponse(
                        user.getId(),
                        user.getName(),
                        user.getEmail()
                ))
                .toList();
    }

    @Transactional
    public UserResponse create(CreateUserRequest request) {
        User user = new User();
        user.setName(request.name());
        user.setEmail(request.email());

        User savedUser = userRepository.save(user);

        return new UserResponse(
                savedUser.getId(),
                savedUser.getName(),
                savedUser.getEmail()
        );
    }
}

Use @Transactional on service methods rather than controller methods.


3. Repository Layer

The repository handles database access.

With Spring Data JPA, you usually define an interface that extends JpaRepository.

package com.example.app.repository;

import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Spring Data JPA automatically provides common methods such as:

findAll()
findById(id)
save(entity)
deleteById(id)

You can also add query methods:

package com.example.app.repository;

import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);

    boolean existsByEmail(String email);
}

You generally do not need to annotate Spring Data repository interfaces with @Repository; Spring detects them automatically.


4. Entity Layer

The entity represents database tables.

Use Jakarta persistence imports:

package com.example.app.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class User {

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

    private String name;

    private String email;
}

Entities should mostly represent persistent state. Avoid putting HTTP-specific logic in entities.


5. DTO Layer

DTOs separate your API contract from your database model.

Request DTO:

package com.example.app.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record CreateUserRequest(
        @NotBlank String name,
        @Email @NotBlank String email
) {
}

Response DTO:

package com.example.app.dto;

public record UserResponse(
        Long id,
        String name,
        String email
) {
}

Using DTOs helps avoid exposing internal entity fields directly through your API.


Recommended Responsibilities

Layer Annotation Responsibility
Controller @RestController, @Controller HTTP request/response handling
Service @Service Business logic, transactions
Repository Spring Data JpaRepository Database access
Entity @Entity Database table mapping
DTO/Form Records/classes with validation API input/output models

Dependency Direction

Keep dependencies flowing one way:

Controller → Service → Repository → Entity

Avoid this:

Repository → Service
Service → Controller
Entity → Controller

For example:

  • A controller can inject a service.
  • A service can inject a repository.
  • A repository should not know about services or controllers.
  • Entities should not depend on web/controller classes.

Best Practices

  1. Use constructor injection
    @Service
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        public OrderService(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    }
    
  2. Keep controllers thin

    Bad:

    @PostMapping
    public User create(@RequestBody User user) {
        if (user.getEmail() == null) {
            throw new IllegalArgumentException("Email is required");
        }
    
        return userRepository.save(user);
    }
    

    Better:

    @PostMapping
    public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }
    
  3. Put transactions in services
    @Transactional
    public UserResponse create(CreateUserRequest request) {
        // business logic and repository calls
    }
    
  4. Use DTOs for API boundaries

    Do not expose entities directly unless the application is very small or internal.

  5. Keep the main application class in the root package

    com.example.app.AppApplication
    

That way Spring can scan:

com.example.app.controller
com.example.app.service
com.example.app.repository
com.example.app.entity

Feature-Based Alternative

For larger applications, you may prefer organizing by feature instead of technical layer:

com.example.app
├── user
│   ├── UserController.java
│   ├── UserService.java
│   ├── UserRepository.java
│   ├── User.java
│   ├── CreateUserRequest.java
│   └── UserResponse.java
├── order
│   ├── OrderController.java
│   ├── OrderService.java
│   ├── OrderRepository.java
│   └── Order.java
└── AppApplication.java

This is often easier to maintain as the project grows because related files stay together.


Simple Rule of Thumb

Ask this when deciding where code belongs:

  • Is it about HTTP? Put it in the controller.
  • Is it business logic? Put it in the service.
  • Is it database access? Put it in the repository.
  • Is it database structure? Put it in the entity.
  • Is it request/response shape? Put it in a DTO.

For most Spring applications, the clean structure is:

Controller → Service → Repository → Database

with DTOs at the API boundary and entities at the persistence boundary.

Understanding @Entity, @Repository, and @Service in Spring Boot

In Spring Boot (and the larger Spring Framework), the annotations @Entity, @Repository, and @Service play a key role in structuring and organizing applications using the principles of dependency injection and inversion of control. Here’s an overview of each:


1. @Entity

  • Definition: The @Entity annotation is used in Java Persistence API (JPA) to define a class as a persistent entity. This means the class maps to a table in the database.
  • Key Features:
    • Marks a POJO (Plain Old Java Object) as a JPA entity.
    • Each annotated class is associated with a database table, and each instance of the class represents a row in that table.
    • Requires a primary key, typically annotated with @Id.
  • Example:

package org.kodejava.spring;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Employee {
    @Id
    private Long id;
    private String name;
    private String role;

    // Getters and setters
}
  • Usage Context: This annotation is part of Jakarta EE (or JPA) and is generally used for classes that model database tables.

2. @Repository

  • Definition: The @Repository annotation indicates that the class is a repository, which is responsible for interacting with the database.
  • Key Features:

    • Used for Data Access Objects (DAO).
    • It helps encapsulate the interaction with the database from the rest of the application.
    • It automatically translates exceptions thrown by the persistence layer into Spring’s unchecked exceptions (like DataAccessException).
  • Example:

package org.kodejava.spring;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    // Custom database queries (if needed)
}
  • Usage Context: Typically, @Repository is used to annotate interfaces or classes that handle data persistence, often enhanced by Spring Data JPA for reducing boilerplate code.

3. @Service

  • Definition: The @Service annotation marks a class as a business service that contains the application’s business logic.
  • Key Features:

    • Indicates that the class is a “service” component in the Service layer.
    • Helps clearly separate business logic from other concerns, such as data persistence or presentation.
    • Works in conjunction with @Component to allow dependency injection.
  • Example:

package org.kodejava.spring;

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class EmployeeService {
    private final EmployeeRepository repository;

    // Constructor injection of the repository
    public EmployeeService(EmployeeRepository repository) {
        this.repository = repository;
    }

    public List<Employee> getAllEmployees() {
        return repository.findAll();
    }

    public Employee saveEmployee(Employee employee) {
        return repository.save(employee);
    }
}
  • Usage Context: Typically used to encapsulate and reuse business logic.

Summary of Their Responsibilities in an Application Layer:

  • @Entity: Maps a Java class to a database table (used in the Data Model layer).
  • @Repository: Handles database operations (typically at the Data Access layer).
  • @Service: Contains business logic (used in the Service layer).

How These Work Together:

These annotations correspond to different tiers in a common layering structure of a Spring Boot application:
1. Entity: Represents data (e.g., Employee).
2. Repository: Provides the CRUD operations for entities using JPA (e.g., EmployeeRepository).
3. Service: Manages the application’s business logic and interactions (e.g., EmployeeService).

By using these annotations together, you achieve a clean separation of concerns, making the application easier to maintain, test, and scale.

How do I create my first Hibernate entity using annotations?

To create a basic Hibernate entity using annotations, follow these steps:

1. Add Required Dependencies

Ensure you have added the required dependencies for Hibernate, JPA (jakarta.persistence), and any database (e.g., H2 for testing) in your pom.xml. For example:

<dependencies>
    <!-- Hibernate Core -->
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.4.4.Final</version>
    </dependency>
    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.2.224</version>
        <scope>runtime</scope>
    </dependency>
    <!-- JPA API -->
    <dependency>
        <groupId>jakarta.persistence</groupId>
        <artifactId>jakarta.persistence-api</artifactId>
        <version>3.1.0</version>
    </dependency>
</dependencies>

2. Create an Entity Class

An entity class represents a table in the database and should be annotated with @Entity. For example:

package org.kodejava.hibernate;

import jakarta.persistence.*;

@Entity
@Table(name = "students")  // Maps to a table named 'students'
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // Auto-incremented primary key
    private Long id;

    @Column(name = "name", nullable = false)  // Maps field to a column
    private String name;

    // Default constructor
    public Student() {}

    // Constructor with arguments
    public Student(String name) {
        this.name = name;
    }

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Explanation of Annotations

  1. @Entity: Marks the class as an entity that maps to a database table.
  2. @Table(name = "table_name"): Specifies the name of the database table (optional). If omitted, the table will use the class name.
  3. @Id: Marks the field as the primary key.
  4. @GeneratedValue(strategy = GenerationType.IDENTITY): Specifies auto-generation of primary key values.
  5. @Column(name = "column_name"): Maps a class field to a specific table column (optional). Omitting this will map the field name to a column with the same name.

3. Specify Entity in Hibernate Configuration

Ensure this entity is configured in your hibernate.cfg.xml file, or programmatically added when building the SessionFactory. In the XML file, include:

<mapping class="org.kodejava.hibernate.Student" />

4. Persist Data Using Hibernate

You can now use Hibernate to perform CRUD operations on this entity. For example, to save a Student:

package org.kodejava.hibernate;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateApp {
    public static void main(String[] args) {
        // Create a SessionFactory and configure Hibernate
        SessionFactory factory = new Configuration()
                .configure("hibernate.cfg.xml")  // Load the configuration file
                .addAnnotatedClass(Student.class)  // Add annotated class
                .buildSessionFactory();

        // Open session
        try (Session session = factory.openSession()) {
            // Create a new student entity
            Student student = new Student("John Doe");

            // Start a transaction
            session.beginTransaction();

            // Save the student to the database
            session.persist(student);

            // Commit the transaction
            session.getTransaction().commit();

            System.out.println("Student saved successfully with ID: " + student.getId());
        } finally {
            factory.close();  // Close the factory
        }
    }
}

Summary

By using annotations like @Entity, @Table, @Id, and @Column, you can define the structure of your database table directly within the Java entity class. Hibernate simplifies interacting with the database and reduces the amount of boilerplate code involved.

How do I delete entity object in JPA?

The following code example show you how to delete or remove entity object from database using JPA. The first class that we are going to create is ArtistDaoImpl which implements ArtistDao. This DAO class handles the delete process either by the entity ID or by the entity object itself. We define the delete process in deleteById(Long id) and delete(Artist artist) methods.

In those methods we call the EntityManager.remove() method. This method of EntityManager will take care of removing the entity object from our database. Let’s see the DAO code below:

package org.kodejava.jpa.dao;

import org.kodejava.jpa.entity.Artist;

import java.util.List;

public interface ArtistDao {
    Artist findById(Long id);

    void save(Artist artist);

    void update(Artist artist);

    List<Artist> getArtists();

    void deleteById(Long id);

    void delete(Artist artist);
}
package org.kodejava.jpa.dao.impl;

import org.kodejava.jpa.dao.ArtistDao;
import org.kodejava.jpa.entity.Artist;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.Query;
import java.util.List;

public class ArtistDaoImpl implements ArtistDao {
    private final EntityManager manager;

    public ArtistDaoImpl(EntityManager manager) {
        this.manager = manager;
    }

    /**
     * Find Artist based on the entity id.
     *
     * @param artistId the artist id.
     * @return Artist.
     * @throws EntityNotFoundException when no artist is found.
     */
    public Artist findById(Long artistId) {
        Artist artist = manager.find(Artist.class, artistId);
        if (artist == null) {
            throw new EntityNotFoundException("Can't find Artist for ID "
                    + artistId);
        }
        return artist;
    }

    @Override
    public void save(Artist artist) {
        manager.getTransaction().begin();
        manager.persist(artist);
        manager.getTransaction().commit();
    }

    /**
     * Update Artist information.
     *
     * @param artist an Artist to be updated.
     */
    @Override
    public void update(Artist artist) {
        manager.getTransaction().begin();
        manager.merge(artist);
        manager.getTransaction().commit();
    }

    @Override
    @SuppressWarnings(value = "unchecked")
    public List<Artist> getArtists() {
        Query query = manager.createQuery("select a from Artist a", Artist.class);
        return query.getResultList();
    }

    /**
     * Delete artist by their id.
     *
     * @param id the artist id.
     */
    @Override
    public void deleteById(Long id) {
        Artist artist = manager.find(Artist.class, id);
        if (artist != null) {
            manager.getTransaction().begin();
            manager.remove(artist);
            manager.getTransaction().commit();
        }
    }

    /**
     * Delete artist entity.
     *
     * @param artist the object to be deleted.
     */
    @Override
    public void delete(Artist artist) {
        manager.getTransaction().begin();
        manager.remove(artist);
        manager.getTransaction().commit();
    }
}

After defining the delete methods in the ArtistDao class we create a simple program to demonstrate both of them. In this program we start by create the EntityManagerFactory object from the defined persistence unit in the persistence.xml file. Then we create the EntityManager object, and we pass it to our ArtistDaoImpl object. And then we call the delete methods to remove entity from the database.

To show you the result of the delete process we print out the artist data before and after the delete method is called.

package org.kodejava.jpa;

import org.kodejava.jpa.dao.ArtistDao;
import org.kodejava.jpa.dao.impl.ArtistDaoImpl;
import org.kodejava.jpa.entity.Artist;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;

public class EntityRemoveDemo {
    public static final String PERSISTENCE_UNIT_NAME = "music";

    public static void main(String[] args) {
        EntityManagerFactory factory =
                Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
        EntityManager manager = factory.createEntityManager();

        ArtistDao dao = new ArtistDaoImpl(manager);
        System.out.println("Before Delete:");
        printArtists(dao.getArtists());

        // Remove artist with ID = 1.
        dao.deleteById(1L);

        // Remove artist with ID = 2.
        Artist artist = dao.findById(2L);
        dao.delete(artist);

        System.out.println("After Delete:");
        printArtists(dao.getArtists());
    }

    private static void printArtists(List<Artist> artists) {
        for (Artist artist : artists) {
            System.out.println("Artist = " + artist);
        }
    }
}

Here is the result of our code snippet. It shows the number of records before and after the delete process.

Before Delete:
Artist = Artist{id=1, name='Bon Jovi'}
Artist = Artist{id=2, name='Mr. Big'}
Artist = Artist{id=3, name='Metallica'}
After Delete:
Artist = Artist{id=3, name='Metallica'}

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>javax.persistence-api</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.6.9.Final</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.1.0</version>
    </dependency>
</dependencies>

Maven Central Maven Central Maven Central

How do I get the primary key of any JPA entity?

If you want to get the primary key of any JPA entity object you can use PersistenceUnitUtil.getIdentifier() method. This method take a single parameter which is the entity object whose identifier to be read. The PersistenceUnitUtil instance can be accessed from the EntityManagerFactory object.

If the entity object contains an identifier the getIdentifier() method will return the identifier as a java.lang.Object. If the entity object does not have an identifier ye it will return null.

package org.kodejava.jpa;

import org.kodejava.jpa.entity.Artist;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class GetEntityIdDemo {
    public static final String PERSISTENCE_UNIT_NAME = "music";

    public static void main(String[] args) {
        EntityManagerFactory factory =
                Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
        EntityManager manager = factory.createEntityManager();

        // Get object identifier of an exists entity.
        Artist artist = manager.find(Artist.class, 1L);
        if (artist != null) {
            Object identifier =
                    factory.getPersistenceUnitUtil().getIdentifier(artist);

            System.out.println("Identifier = " + identifier);
        }

        // Get object identifier of a newly inserted entity.
        Artist newArtist = new Artist();
        newArtist.setName("Bon Jovi");

        manager.getTransaction().begin();
        manager.persist(newArtist);
        manager.getTransaction().commit();

        Object identifier =
                factory.getPersistenceUnitUtil().getIdentifier(newArtist);
        System.out.println("Identifier = " + identifier);
    }
}

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>javax.persistence-api</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.6.9.Final</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.1.0</version>
    </dependency>
</dependencies>

Maven Central Maven Central Maven Central