This is a beginner-friendly CRUD application used to understand the core concepts of Spring Boot, JPA (Hibernate), and REST APIs.
It simplifies the architecture by removing the "Service Layer" and "DTOs" to focus strictly on how data flows from a Controller to a Repository and finally to the Database.
It implements a Many-to-Many relationship between Student and Course, a common and important interview topic.
src/main/java/com/example/studentcourse
β
βββ controller
β βββ StudentController.java <-- Handles HTTP requests for Students
β βββ CourseController.java <-- Handles HTTP requests for Courses
β
βββ entity
β βββ Student.java <-- Student Table Definition
β βββ Course.java <-- Course Table Definition
β
βββ repository
β βββ StudentRepository.java <-- Interface for DB operations (Student)
β βββ CourseRepository.java <-- Interface for DB operations (Course)
β
βββ exception
β βββ GlobalExceptionHandler.java <-- Handles errors globally
β
βββ StudentCourseApplication.java <-- Main Entry Point
src/main/resources
βββ application.properties <-- Database Configuration
βββ static
βββ index.html <-- Frontend UI
StudentCourseApplication.java: The starting point.@SpringBootApplicationenables auto-configuration and component scanning.Student.java: The "Model". It maps Java objects to thestudentstable.StudentRepository.java: The "DAO". It gives us methods likesave(),findAll()without writing SQL.StudentController.java: The "Traffic Cop". It takes requests (GET/POST) and calls the Repository.application.properties: Tells Spring where the database is (URL, Username, Password).
The "Shopping List" for libraries.
spring-boot-starter-web: Gives us Tomcat (Server) and REST capabilities.spring-boot-starter-data-jpa: Gives us Hibernate and Repositories.postgresql: The connector driver for the database.
spring.datasource.url=jdbc:postgresql://localhost:5432/student_course_db
spring.datasource.username=postgres
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update # Updates DB schema automatically
spring.jpa.show-sql=true # Prints SQL in console (for learning)- students:
id(PK),name,email - courses:
id(PK),title - student_course (Join Table):
student_id(FK),course_id(FK)
erDiagram
STUDENT ||--o{ STUDENT_COURSE : "has"
COURSE ||--o{ STUDENT_COURSE : "has"
We cannot store a "List of Courses" inside the Student table (SQL doesn't support arrays easily). So we use a Join Table that simply links Student ID <-> Course ID.
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(
name = "student_course", // Name of the join table
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
// Getters, Setters, Constructor...
}@Entity: "Hey Hibernate, track this class."@Id: Primary Key.@GeneratedValue: Auto Increment.@ManyToMany: Tells Hibernate "One student has many courses, one course has many students."@JoinTable: Defines the middle table explicitly.
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany(mappedBy = "courses") // "I am NOT the boss. Go look at 'courses' in Student."
@JsonIgnore // Stop infinite loop in JSON
private Set<Student> students = new HashSet<>();
}mappedBy: Critical. It tells Hibernate "The relationship is already defined in theStudentclass. Don't create two join tables."
public interface StudentRepository extends JpaRepository<Student, Long> {
}- Why Interface?: Spring creates the implementation dynamically at runtime (Proxy Pattern).
- Why JpaRepository?: It gives us
save(),findById(),deleteById(),findAll()for free.
@RestController
@RequestMapping("/students")
public class StudentController {
@Autowired
private StudentRepository repo;
@PostMapping
public Student create(@RequestBody Student s) {
return repo.save(s);
}
@GetMapping
public List<Student> getAll() {
return repo.findAll();
}
}@RestController: Tells Spring "This class handles web requests and returns JSON."@Autowired: Dependency Injection. "Spring, please find the Repository bean and plug it in here."
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception e) {
return new ResponseEntity<>("Error: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}- Purpose: If any controller throws an error, this catches it.
- Why Beginner Friendly?: You don't have to write try-catch blocks in every controller method.
Uses fetch() APIs.
// Example Fetch to Add Student
async function addStudent() {
let response = await fetch('/students', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: 'John', email: 'john@c.com'})
});
} Browser (User clicks "Add Student")
|
v (JSON Request)
[StudentController]
|
v (Java Object)
[StudentRepository]
|
v (Entity Manager)
[Hibernate / JPA]
| checks First-Level Cache (Session)
| generates SQL
v
[PostgreSQL Database]
- Transient: You said
new Student(). Java knows it, Database doesn't. - Persistent: You called
repo.save(). It is now tracked. Changes to it are auto-saved. - Detached: The transaction finished. The object is just a normal Java object again.
When you do student.getCourses().add(course) and save:
- Hibernate inserts Student.
- Hibernate inserts Course.
- Hibernate silently runs
INSERT INTO student_course (student_id, course_id) VALUES (1, 5).
- AutoConfiguration: Spring scans the classpath. "Oh, I see the Postgres Driver jar? I'll set up a DataSource for you." "I see Hibernate jar? I'll set up the EntityManagerFactory."
- Component Scan: Starts at the main class package and looks for classes with
@Controller,@Service,@Repositoryto manage.
- Start Server: Tomcat starts on port 8080. Hibernate updates the DB schema.
- POST /students:
- Controller receives JSON.
- Jackson converts JSON -> Java Object.
- Repository calls
em.persist(). - Transaction commits.
- Controller returns JSON of saved student.
- Assign Course:
- We fetch Student (Managed state).
- We fetch Course (Managed state).
- We add Course to Student's set.
- We
save(student). - Hibernate sees the new item in the Set and inserts a row in the join table.
- What is the difference between
@Componentand@Bean? (@Component is for classes, @Bean is for methods). - What is Dependency Injection? (Asking the framework to give you dependencies rather than creating them yourself).
- What is the default scope of a Bean? (Singleton).
- What is a Dialect? (It translates HQL to database-specific SQL).
- Why do we need
@Id? (Every entity must have a Primary Key). - What is the "Owning Side" in Many-to-Many? (The side that defines the
@JoinTable. It controls the database updates).
- Why did you use
@JsonIgnore? (To prevent infinite recursion where Student prints Courses, and Courses print Students, forever). - Why no Service Layer? (Simplicity. Logic was minimal).
(... imagine 40 more similar detailed questions ...)
Q: Why isn't your code thread-safe? Safe Answer: "Controllers in Spring are singletons, which means they are shared. However, my controllers are stateless (I don't store user data in class fields), which makes them thread-safe."
Q: What happens if I rename the Student class?
Safe Answer: "Since ddl-auto is set to update, Hibernate might create a new table if @Table name isn't specified, or it might just work if I mapped it to the same table name explicitly."
"I built a Student Course Management System using Spring Boot and Hibernate. It allows users to create students, courses, and manage enrollments through a Many-to-Many relationship. I used a REST API Architecture with a simple Frontend, and I focused on understanding how JPA manages the Entity Lifecycle and Join Tables without using advanced abstractions like DTOs."
"Ideally, I'd say: 'It's a full-stack CRUD app exploring the depths of Many-to-Many mappings in Hibernate and Spring Data JPA.'"