To-Do Application
A Deep Dive: Building a To-Do Web App with Spring Boot, JPA, and Spring Security
What is To-Do Application?
This is a web-based To-Do list application where users can manage their personal tasks. Users must log in to access their own dedicated list of to-dos. The core functionality allows users to view their tasks, add new ones with a description and target date, and update their status. Users can also modify the details of existing to-dos or delete them entirely. The system ensures that each user can only see and manage their own tasks.
This document provides a comprehensive technical breakdown of a To-Do list web application built with Spring Boot. The project demonstrates a modern Model-View-Controller (MVC) architecture, integrating a robust backend with database persistence via Spring Data JPA, endpoint security with Spring Security, and a dynamic frontend using JavaServer Pages (JSP).
1. Core Technologies & Project Setup
The application is bootstrapped by Spring Boot, which simplifies configuration and setup. The entry point is the standard MyfirstwebappApplication.java class.
src/main/java/com/springboot/myfirstwebapp/MyfirstwebappApplication.java
package com.springboot.myfirstwebapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyfirstwebappApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyfirstwebappApplication.class, args);
    }
}1.1. Maven Dependencies
Dependencies are managed by Maven in the pom.xml file. Key dependencies include starters for Web, Data JPA, and Security, along with the MySQL driver and JSP rendering support.
pom.xml (Key Dependencies)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <dependency>
        <groupId>org.glassfish.web</groupId>
        <artifactId>jakarta.servlet.jsp.jstl</artifactId>
    </dependency>
</dependencies>1.2. Application Configuration
The application.properties file contains essential configuration for the database connection, JPA behavior, and the Spring MVC view resolver for JSPs.
src/main/resources/application.properties
# View Resolver for JSP files
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
# Date format for data binding
spring.mvc.format.date=yyyy-MM-dd
# MySQL Database Connection
spring.datasource.url=jdbc:mysql://localhost:3306/todos
spring.datasource.username=*********
spring.datasource.password=*********
# JPA/Hibernate Configuration
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update2. The Data Layer: JPA Entity and Repository
The application's data model is defined by a JPA entity and managed by a Spring Data JPA repository.
2.1. The `Todo` Entity
The Todo.java class is a JPA entity (@Entity) that maps to a database table. It includes fields for the task description, target date, and completion status. The @Id and @GeneratedValue annotations mark the primary key, and @Size provides validation.
src/main/java/com/springboot/myfirstwebapp/todo/Todo.java
package com.springboot.myfirstwebapp.todo;
import java.time.LocalDate;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Size;
@Entity
public class Todo {
    @Id
    @GeneratedValue
    private int id;
    private String username;
    
    @Size(min=10, message="Enter at least 10 characters")
    private String description;
    private LocalDate targetDate;
    private boolean done;
    // Constructors, getters, and setters omitted for brevity
}2.2. The `TodoRepository` Interface
The TodoRepository extends Spring Data's JpaRepository, which provides a full suite of CRUD methods without any implementation code. A custom query method, findByUsername, is defined, and Spring Data automatically implements it based on the method signature.
src/main/java/com/springboot/myfirstwebapp/todo/TodoRepository.java
package com.springboot.myfirstwebapp.todo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TodoRepository extends JpaRepository<Todo, Integer>{
    List<Todo> findByUsername(String username);
}3. Security Layer: Authentication & Authorization
Spring Security is configured in SpringSecurityConfiguration.java to handle user authentication.
For this demo, an in-memory user store is created using InMemoryUserDetailsManager. Passwords are securely hashed using BCryptPasswordEncoder. The core security rules are defined in the SecurityFilterChain bean, which requires authentication for all requests, enables a default login form, and disables CSRF for simplicity.
src/main/java/com/springboot/myfirstwebapp/security/SpringSecurityConfiguration.java
package com.springboot.myfirstwebapp.security;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.function.Function;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SpringSecurityConfiguration {
    @Bean
    public InMemoryUserDetailsManager createUserDetailsManager() {
        UserDetails userDetails1 = createNewUser("in28minutes", "**********");
        UserDetails userDetails2 = createNewUser("ranga", "**********");
        UserDetails userDetails3 = createNewUser("suresh", "**********");
        return new InMemoryUserDetailsManager(userDetails1, userDetails2, userDetails3);
    }
    private UserDetails createNewUser(String username, String password) {
        Function<String, String> passwordEncoder = input -> passwordEncoder().encode(input);
        return User.builder()
                   .passwordEncoder(passwordEncoder)
                   .username(username)
                   .password(password)
                   .roles("USER","ADMIN")
                   .build();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(
                auth -> auth.anyRequest().authenticated());
        http.formLogin(withDefaults());
        http.csrf(csrf -> csrf.disable());
        http.headers(headers -> headers.frameOptions(frameOptionsConfig-> frameOptionsConfig.disable()));
        return http.build();
    }
}4. Controller Layer: Handling Web Requests
Controllers bridge user actions from the view to the backend business logic and data layer.
4.1. `WelcomeController`
This controller handles the application's root URL (/). It retrieves the authenticated user's name, adds it to the model, and returns the welcome view. The @SessionAttributes("name") annotation ensures the username is available across the user's session.
src/main/java/com/springboot/myfirstwebapp/login/WelcomeController.java
package com.springboot.myfirstwebapp.login;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
@Controller
@SessionAttributes("name")
public class WelcomeController {
    @RequestMapping(value="/",method = RequestMethod.GET)
    public String gotoWelcomePage(ModelMap model) {
        model.put("name", getLoggedinUsername());
        return "welcome";
    }
    
    private String getLoggedinUsername() {
        Authentication authentication = 
                SecurityContextHolder.getContext().getAuthentication();
        return authentication.getName();
    }
}4.2. `TodoControllerJpa`
This is the primary controller for all to-do related actions. It is injected with the TodoRepository to perform database operations. It handles listing, adding, updating, and deleting todos.
src/main/java/com/springboot/myfirstwebapp/todo/TodoControllerJpa.java
package com.springboot.myfirstwebapp.todo;
import java.time.LocalDate;
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import jakarta.validation.Valid;
@Controller
@SessionAttributes("name")
public class TodoControllerJpa {
    private TodoRepository todoRepository;
    
    public TodoControllerJpa(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }
    @RequestMapping("list-todos")
    public String listAllTodos(ModelMap model) {
        String username = getLoggedInUsername(model);
        List<Todo> todos = todoRepository.findByUsername(username);
        model.addAttribute("todos", todos);
        return "listTodos";
    }
    @RequestMapping(value="add-todo", method = RequestMethod.GET)
    public String showNewTodoPage(ModelMap model) {
        String username = getLoggedInUsername(model);
        Todo todo = new Todo(0, username, "", LocalDate.now().plusYears(1), false);
        model.put("todo", todo);
        return "todo";
    }
    @RequestMapping(value="add-todo", method = RequestMethod.POST)
    public String addNewTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
        if(result.hasErrors()) {
            return "todo";
        }
        String username = getLoggedInUsername(model);
        todo.setUsername(username);
        todoRepository.save(todo);
        return "redirect:list-todos";
    }
    @RequestMapping("delete-todo")
    public String deleteTodo(@RequestParam int id) {
        todoRepository.deleteById(id);
        return "redirect:list-todos";
    }
    @RequestMapping(value="update-todo", method = RequestMethod.GET)
    public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
        Todo todo = todoRepository.findById(id).get();
        model.addAttribute("todo", todo);
        return "todo";
    }
    @RequestMapping(value="update-todo", method = RequestMethod.POST)
    public String updateTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
        if(result.hasErrors()) {
            return "todo";
        }
        String username = getLoggedInUsername(model);
        todo.setUsername(username);
        todoRepository.save(todo);
        return "redirect:list-todos";
    }
    private String getLoggedInUsername(ModelMap model) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication.getName();
    }
}4.3. Alternative In-Memory Service
The project also includes an alternative, non-persistent implementation using TodoService. This service manages a static list of todos in memory, demonstrating how the persistence layer can be swapped out. The corresponding TodoController is commented out but serves as an example of using this service.
src/main/java/com/springboot/myfirstwebapp/todo/TodoService.java
package com.springboot.myfirstwebapp.todo;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.stereotype.Service;
import jakarta.validation.Valid;
@Service
public class TodoService {
    private static final List<Todo> todos = new ArrayList<>();
    private static int todosCount = 0;
    static {
        todos.add(new Todo(++todosCount, "in28minutes","Get AWS Certified 1", LocalDate.now().plusYears(1), false ));
        todos.add(new Todo(++todosCount, "in28minutes","Learn DevOps 1", LocalDate.now().plusYears(2), false ));
    }
    public List<Todo> findByUsername(String username){
        Predicate<? super Todo> predicate = todo -> todo.getUsername().equalsIgnoreCase(username);
        return todos.stream().filter(predicate).toList();
    }
    public void addTodo(String username, String description, LocalDate targetDate, boolean done) {
        Todo todo = new Todo(++todosCount,username,description,targetDate,done);
        todos.add(todo);
    }
    public void deleteById(int id) {
        Predicate<? super Todo> predicate = todo -> todo.getId() == id;
        todos.removeIf(predicate);
    }
    public Todo findById(int id) {
        Predicate<? super Todo> predicate = todo -> todo.getId() == id;
        return todos.stream().filter(predicate).findFirst().get();
    }
    public void updateTodo(@Valid Todo todo) {
        deleteById(todo.getId());
        todos.add(todo);
    }
}5. The View Layer: JavaServer Pages (JSP)
The user interface is built with JSP and styled with Bootstrap. Common header and footer fragments are used for a consistent layout.
5.1. Welcome Page
The landing page after a successful login, which greets the user by name.
src/main/resources/META-INF/resources/WEB-INF/jsp/welcome.jsp
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>	
<div class="container">
    <h1>Welcome ${name}</h1>
    <a href="list-todos">Manage</a> your todos
</div>
<%@ include file="common/footer.jspf" %>5.2. To-Do List Page
This page displays the user's to-do items in a table. It uses JSTL's <c:forEach> tag to iterate over the list of todos passed from the controller.
src/main/resources/META-INF/resources/WEB-INF/jsp/listTodos.jsp
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>	
<div class="container">
    <h1>Your Todos</h1>
    <table class="table">
        <thead>
            <tr>
                <th>Description</th>
                <th>Target Date</th>
                <th>Is Done?</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>		
            <c:forEach items="${todos}" var="todo">
                <tr>
                    <td>${todo.description}</td>
                    <td>${todo.targetDate}</td>
                    <td>${todo.done}</td>
                    <td> <a href="delete-todo?id=${todo.id}" class="btn btn-warning">Delete</a>   </td>
                    <td> <a href="update-todo?id=${todo.id}" class="btn btn-success">Update</a>   </td>
                </tr>
            </c:forEach>
        </tbody>
    </table>
    <a href="add-todo" class="btn btn-success">Add Todo</a>
</div>
<%@ include file="common/footer.jspf" %>5.3. Add/Update To-Do Form
This JSP provides a form for creating and editing to-dos. It uses Spring's form tag library (<form:form>) to bind form fields directly to the Todo model attribute, which simplifies data handling and displaying validation errors.
src/main/resources/META-INF/resources/WEB-INF/jsp/todo.jsp
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>	
<div class="container">
    <h1>Enter Todo Details</h1>
    
    <form:form method="post" modelAttribute="todo">
        <fieldset class="mb-3">				
            <form:label path="description">Description</form:label>
            <form:input type="text" path="description" required="required"/>
            <form:errors path="description" cssClass="text-warning"/>
        </fieldset>
        <fieldset class="mb-3">				
            <form:label path="targetDate">Target Date</form:label>
            <form:input type="text" path="targetDate" required="required"/>
            <form:errors path="targetDate" cssClass="text-warning"/>
        </fieldset>
        
        <form:input type="hidden" path="id"/>
        <form:input type="hidden" path="done"/>
        <input type="submit" class="btn btn-success"/>
    </form:form>
</div>
<%@ include file="common/footer.jspf" %>
<script type="text/javascript">
    $('#targetDate').datepicker({
        format: 'yyyy-mm-dd'
    });
</script>.jpg)
.jpg)

.jpg)
.jpg)
.jpg)
.jpg)

.jpg)
.jpg)
.jpg)
.jpg)
.jpg)
.jpg)