Development Process of the Java EE Web App.
Here, I am using the Layered Architecture to maintain my code cleanly. This architecture can also be identified as Multitier Architecture…
Development Process of the Java EE Web App.

Here, I am using the Layered Architecture to maintain my code cleanly. This architecture can also be identified as Multitier Architecture or N-Tier Architecture. So, let’s figure it out.
Layered Architecture has mainly 6 parts. They can be pointed out as,
DTO → Controller → Service → Mapper → Model → DAO
🧱 1. DTO (Data Transfer Object)
The purpose of the DTO layer is to carry data between layers. (especially between frontend and backend, or between controller and service). This can’t be considered as a separate layer; this is a pattern used to avoid exposing internal model entities.
🧭 2. Controller Layer
This layer can also be identified as the presentation layer or web layer. Handling HTTP requests/responses, delegating the business logic to the service layer, and etc can be pointed out as the responsibilities of this layer. The purpose of this layer is to take data fromthe DTO and send those to the service layer.
⚙️ 3. Service Layer
This layer can also be identified as the service layer. This is the place that contains our business logic. The service layer calls the Mapper to convert the DTO into a real Model (Entity), then asks the DAO to save it.
🔄 4. Mapper Layer
This layer can also be identified as the Conversion or Adapter Layer. We can’t use DTO directly to save in the database. Because we have to use this layer to convert DTO to Model (and Model to DTO if needed).
🧬 5. Model Layer
This layer can also be identified as the Domain Model or Entity Layer. Represents the real-world business objects or database table structures can be pointed out as the main purpose of this layer.
🗃️ 6. DAO (Data Access Object)
This layer can also be identified as the Persistence Layer or Repository Layer. We can use this layer to save, find, or delete things in our database.
This flow describes the direction of data flow at runtime, not the order you must code it. So, what is the development flow?

We should build the program from the bottom up, like constructing a building. For that, we can use the above flow.
Coding Part 👨💻
Now let’s see how to register an admin in our system using this Layered Architecture step by step.
1. Model Layer
1.1 UserModel
package org.example.pahana_edu.model;
import java.time.LocalDateTime;
public class UserModel {
private Integer id;
private String username;
private String email;
private String password;
private String firstName;
private String lastName;
// Constructor with essential fields
public UserModel(String username, String email, String password, String firstName, String lastName) {
this.username = username;
this.email = email;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
}
Before developing the Service Layer (UserService), let’s consider the UserModel, because we have to import the UserModel when dealing with the UserService. The UserModel class is like a database blueprint for a user. It defines the structure of a user’s data (like their ID, username, email, password, and names) as it’s stored in the database.
After defining the UserModel, we have six private variables. And we can consider these as real user inputs. These are the details that are stored in the database. When it comes to the constructor, it creates a UserModel object and fills most fields (exceptid, which is usually set by the database). For example, new UserModel(“john123”, “john@example.com”, “hashedPassword”, “John”, “Doe”) creates a user object.
And then we have getters and setters. We can use these to see and change the values of each private variable in the UserModel class. And finally, overrides the toString() method to return a readable string representation of the UserModel object, like User{id=12345, username=’john123', email=’john@example.com’, firstName=’John’, lastName=’Doe’}. It excludes the password for security.
2. DAO (Data Access Object)
2.1 UserDAO
package org.example.pahana_edu.dao;
import org.example.pahana_edu.model.UserModel;
import org.example.pahana_edu.util.DBConn;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class UserDAO {
public UserModel save(UserModel user) throws SQLException {
String sql = "INSERT INTO admins (username, email, password, first_name, last_name) VALUES (?, ?, ?, ?, ?)";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getEmail());
stmt.setString(3, user.getPassword());
stmt.setString(4, user.getFirstName());
stmt.setString(5, user.getLastName());
int affectedRows = stmt.executeUpdate();
if (affectedRows == 0) {
throw new SQLException("Creating user failed, no rows affected.");
}
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
user.setId((int) generatedKeys.getLong(1));
} else {
throw new SQLException("Creating user failed, no ID obtained.");
}
}
}
return user;
}
public Optional<UserModel> findByUsername(String username) throws SQLException {
String sql = "SELECT * FROM admins WHERE username = ?";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToUser(rs));
}
}
}
return Optional.empty();
}
public Optional<UserModel> findByEmail(String email) throws SQLException {
String sql = "SELECT * FROM admins WHERE email = ?";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToUser(rs));
}
}
}
return Optional.empty();
}
public Optional<UserModel> findById(Long id) throws SQLException {
String sql = "SELECT * FROM admins WHERE id = ?";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setLong(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToUser(rs));
}
}
}
return Optional.empty();
}
public List<UserModel> findAll() throws SQLException {
String sql = "SELECT * FROM admins";
List<UserModel> users = new ArrayList<>();
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
users.add(mapResultSetToUser(rs));
}
}
return users;
}
public boolean existsByUsername(String username) throws SQLException {
String sql = "SELECT COUNT(*) FROM admins WHERE username = ?";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1) > 0;
}
}
}
return false;
}
public boolean existsByEmail(String email) throws SQLException {
String sql = "SELECT COUNT(*) FROM admins WHERE email = ?";
try (Connection conn = DBConn.getInstance().getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1) > 0;
}
}
}
return false;
}
private UserModel mapResultSetToUser(ResultSet rs) throws SQLException {
UserModel user = new UserModel();
user.setId((int) rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setPassword(rs.getString("password"));
user.setFirstName(rs.getString("first_name"));
user.setLastName(rs.getString("last_name"));
return user;
}
}
The UserDAO class is like a librarian who manages the school’s student records in a database. Saving new users to the database, finding users by their username, email, or ID, checking if a username or email is already taken, and retrieving all users from the database can be pointed out as the responsibilities of this part.
It uses UserModel objects to represent users in the database and communicates with the database using SQL queries through a DBConn utility class. The UserService calls UserDAO methods to perform these database operations.
First, we have the a method to save a new user to the database, taking a UserModel object and returning it with the generated ID. It can throw an SQLException if the database fails. Then, gets the auto-generated ID from the database (e.g., the new user’s ID) and sets it in the UserModel using setId. If no ID is returned, it throws an error.
Then we have the findByUsername method. This method takes a username as input, queries the admins table to find a user with that username, and returns an Optional<UserModel> meaning it may return a user or nothing. It uses a prepared SQL statement to prevent SQL injection and establishes a connection using DBConn.getInstance().getConnection().
We also have findByEmail and findById methods, which can be used to find a user by email or user ID. Then, we have the findAll method to retrieve all users from the admin table. Here, I have used an ArrayList and UserModel objects to manage multiple users. And finally, we have themapResultSetToUser method. This is a helper method to convert a database ResultSet (raw query results) to a UserModel object. It creates a new UserModel and sets its fields using data from the database.
3. Service Layer
3.1 UserService
package org.example.pahana_edu.service;
import org.example.pahana_edu.dao.UserDAO;
import org.example.pahana_edu.dto.UserLoginDTO;
import org.example.pahana_edu.dto.UserRegistrationDTO;
import org.example.pahana_edu.dto.UserResponseDTO;
import org.example.pahana_edu.mapper.UserMapper;
import org.example.pahana_edu.model.UserModel;
import org.example.pahana_edu.util.PasswordUtil;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class UserService {
private final UserDAO userDAO;
public UserService() {
this.userDAO = new UserDAO();
}
public UserResponseDTO registerUser(UserRegistrationDTO registrationDTO) throws SQLException {
// Validate input
if (!registrationDTO.isValid()) {
throw new IllegalArgumentException("Invalid registration data");
}
// Check if username already exists
if (userDAO.existsByUsername(registrationDTO.getUsername())) {
throw new IllegalArgumentException("Username already exists");
}
// Check if email already exists
if (userDAO.existsByEmail(registrationDTO.getEmail())) {
throw new IllegalArgumentException("Email already exists");
}
// Hash password
String hashedPassword = PasswordUtil.hashPassword(registrationDTO.getPassword());
registrationDTO.setPassword(hashedPassword);
// Convert DTO to Entity
UserModel user = UserMapper.toEntity(registrationDTO);
// Save user
UserModel savedUser = userDAO.save(user);
// Convert to response DTO
return UserMapper.toResponseDTO(savedUser);
}
public UserResponseDTO loginUser(UserLoginDTO loginDTO) throws SQLException {
// Validate input
if (!loginDTO.isValid()) {
throw new IllegalArgumentException("Invalid login credentials");
}
// Find user by username
Optional<UserModel> userOptional = userDAO.findByUsername(loginDTO.getUsername());
if (userOptional.isEmpty()) {
throw new IllegalArgumentException("Invalid username or password");
}
UserModel user = userOptional.get();
// Verify password
if (!PasswordUtil.verifyPassword(loginDTO.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("Invalid username or password");
}
// Convert to response DTO
return UserMapper.toResponseDTO(user);
}
}
The UserService class handles the business logic for user-related actions in the application. It uses the DTOs to carry data, a UserDAO to talk to the database, and a UserMapper to convert between DTOs and database models (UserModel). It also handles password security with PasswordUtil.
When it comes to the importing section, there are a few things that we have to pay attention. Let’s see what those are.
UserDAO: For database operations (like saving or finding users).DTOs: The baskets (UserLoginDTO,UserRegistrationDTO,UserResponseDTO) from your earlier code.UserMapper: Converts betweenDTOsandUserModel(a database entity).UserModel: Represents a user in the database.PasswordUtil: Handles password hashing and verification.SQLException,List,Optional,Collectors: Java tools for database errors, lists, optional values, and stream processing.
After creating the class, I have created a private field for a UserDAO object. Because of the final keyword, we can’t change it after set. This is the database helper the service will use. Then, the constructor creates a new UserDAO object when a UserService object is made.
Then we have the registerUser method. This method handles user registration, taking a UserRegistrationDTO (the registration basket) and returning a UserResponseDTO (the user’s profile). It can throw a SQLException if the database fails. Then, hashes the password (turns it into a secure, unreadable form) using PasswordUtil and updates the registrationDTO with the hashed password. After that we can convert the UserRegistrationDTO to a UserModelusing UserMapper using theUserModel user = UserMapper.toEntity(registrationDTO); line.
And then, we have the loginUser method to handle the user login process. The internal process of this method is the same as the registerUser method.
4. DTO (Data Transfer Object)
4.1 UserLoginDTO
package org.example.pahana_edu.dto;
public class UserLoginDTO {
private String username;
private String password;
// Constructor
public UserLoginDTO(String username, String password) {
this.username = username;
this.password = password;
}
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
// Validation method
public boolean isValid() {
return username != null && !username.trim().isEmpty() &&
password != null && !password.trim().isEmpty();
}
}
The purpose of this code is to contain the information that we need to log in. Usually, we use our username or email and the password to log into a system. Here, I have used the username and the password. UserLoginDTO is a public class, which means we can use this in other parts of the program.
First, we have two private variables to keep the login credentials. This can be pointed out as part of one of the OOP methods called Encapsulation. Encapsulation is like putting your data in a locked box (the class) and only allowing access through specific “keys” (methods). In UserLoginDTO, we can’t directly change the username, we must use setUsername() for it.
Then we have the constructor. This lets us create a basket and fill it with a username and password right away. For example, you could say, new UserLoginDTO(“john123”, “secret123”), and it would make a basket with those values. The this.username part refers to the basket’s username slot, and it sets it to whatever value you pass in (like “john123”).
Then we have getters and setters. First, let’s consider the getUsername getter method. This can be considered as a door that lets other parts of the program see what’s in the username slot. Then we have the setUsername setter method. This lets us put a new value into the username slot.
Finally, we have a method called isValid(). It checks if the basket has valid login information. It returns true (yes) if,
- The
usernameisn’t null (meaning it exists) - The
usernameisn’t empty (after removing extra spaces with trim()) - The
passwordisn’t null and - The
passwordisn’t empty.
4.2 UserRegistrationDTO
package org.example.pahana_edu.dto;
public class UserRegistrationDTO {
private String username;
private String email;
private String password;
private String confirmPassword;
private String firstName;
private String lastName;
// Constructor
public UserRegistrationDTO(String username, String email, String password,
String confirmPassword, String firstName, String lastName) {
this.username = username;
this.email = email;
this.password = password;
this.confirmPassword = confirmPassword;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
// Validation method
public boolean isValid() {
return username != null && !username.trim().isEmpty() &&
email != null && !email.trim().isEmpty() &&
password != null && !password.trim().isEmpty() &&
confirmPassword != null && password.equals(confirmPassword) &&
firstName != null && !firstName.trim().isEmpty() &&
lastName != null && !lastName.trim().isEmpty();
}
}
This class can be considered as a basket that holds the information that we need when someone registers for a new account. This basket has some slots named username, email, password, confirmPassword, firstName, and lastName.
First, we have private variables for the above-mentioned slots. These six variables can be directly accessed only from this UserRegistrationDTOclass, because those are private variables.
Then we have the constructor. This lets us to create the user registration basket and fill all six slots at once. Then we have getters and setters. We can use these to see the values of each slot(getters), and to change the values of each slot(setters).
As an example, if you need to see what the value of the username slot is from another class, you have to use getUsername for that. If you need to change the value of the username slot from another class, you have to setUsername for that. And finally, we have the isValid() method to check if the registration basket is filled out correctly.
4.3 UserRegistrationDTO
package org.example.pahana_edu.dto;
public class UserResponseDTO {
private Integer id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructor
public UserResponseDTO(Integer id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
This class is a basket that holds information about a user to send back to the dashboard page after they log in or register. This UserResponse basket has five private slots like ID, username, email, firstName, and lastName.
Then we have the constructor. This lets us create the basket and fill all slots at once. And then we can use getters and setters to see or update each of these slot values from another class.
As a summary, UserLoginDTO can be used to hold the user login form details. And UserRegistrationDTO can be used to hold the user registration form details. And finally, the UserResponseDTO can be used to hold the details that we need to show on the dashboard page or user profile page.
5. Mapper Layer
5.1 UserMapper
package org.example.pahana_edu.mapper;
import org.example.pahana_edu.dto.UserRegistrationDTO;
import org.example.pahana_edu.dto.UserResponseDTO;
import org.example.pahana_edu.model.UserModel;
public class UserMapper {
public static UserModel toEntity(UserRegistrationDTO dto) {
if (dto == null) {
return null;
}
return new UserModel(
dto.getUsername(),
dto.getEmail(),
dto.getPassword(),
dto.getFirstName(),
dto.getLastName()
);
}
public static UserResponseDTO toResponseDTO(UserModel user) {
if (user == null) {
return null;
}
return new UserResponseDTO(
Math.toIntExact(user.getId()),
user.getUsername(),
user.getEmail(),
user.getFirstName(),
user.getLastName()
);
}
}
The UserMapper class is like a translator at a school who converts a student’s application form (UserRegistrationDTO) into an official student record (UserModel) for the database, or turns a student record into a summary (UserResponseDTO) to show on the website.
After creating the class, I have created a static method that converts a UserRegistrationDTO (web form data) to a UserModel (database record). It’s public so UserService can call it, and static means you call it directly (e.g., UserMapper.toEntity(dto)) without creating a UserMapper object.
Then we have another static method called toResponseDTO. This method converts the UserModel (from the database) to a UserResponseDTO (for the web).
6. Controller Layer
6.1 AuthController
package org.example.pahana_edu.controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.example.pahana_edu.dto.UserLoginDTO;
import org.example.pahana_edu.dto.UserRegistrationDTO;
import org.example.pahana_edu.dto.UserResponseDTO;
import java.io.IOException;
import java.sql.SQLException;
@WebServlet(name = "authController", urlPatterns = {"/auth/*"})
public class AuthController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String pathInfo = request.getPathInfo();
if (pathInfo == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
switch (pathInfo) {
case "/login":
request.getRequestDispatcher("/login.jsp").forward(request, response);
break;
case "/register":
request.getRequestDispatcher("/register.jsp").forward(request, response);
break;
case "/logout":
handleLogout(request, response);
break;
case "/dashboard":
handleDashboard(request, response);
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
private void handleLogout(HttpServletRequest request, HttpServletResponse response)
throws IOException {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
private void handleDashboard(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect(request.getContextPath() + "/auth/login");
return;
}
request.getRequestDispatcher("/dashboard.jsp").forward(request, response);
}
}
@WebServlet(name = "authController", urlPatterns = {"/auth/*"})
This is an annotation that tells the web server, “Hey, if someone visits a URL starting with /auth/ (like /auth/login or /auth/register), send them to this servlet.”
The AuthController is a Java Servlet that acts as the brain for handling user-related actions on our web system, like showing login, registration, and dashboard pages, processing the login and registration processes. But before handling login and registration process, we have to create our UserService and UserMapper files because we have to convert the DTOs into a real Model.
The doGet() method handles GET requests, which happen when someone visits a webpage (like typing /auth/login in the browser). The request carries info about what the user wants, and the response is used to send back a page or redirect. The @Override means it’s customizing a method from HttpServlet.
Then we have a String type variable called pathInfo. This gets the part of the URL after /auth/. For example, if the URL is/auth/login, pathInfo is /login. It’s like asking, “Which specific action does the user want?” If pathInfo is null (meaning the URL is just /auth/ with nothing after it), the servlet redirects the user to the login.jsp (the login page). The request.getContextPath() adds the website’s base URL. According to my project, the base URL is /Pahana_Edu_war_exploded. Then we can navigate the user using the switch case below.
And we have the handleLogout method. According to its name, we can use this for the logout process. When it comes to the HttpSession, If there is a session, we can destroy it using invalidate(). After the logout process is successful, the user will fall into the login page.
And finally, we have the handleDashboard page. This method handles showing the dashboard page. When it comes to the HttpSession section, it checks if the user has a session and if the user attribute (the UserResponseDTO) exists. If not, they’re not logged in, so it redirects them to the login page.
메타데이터
- post_id
- f347edbd7b10
- slug
- development-process-of-the-java-ee-web-app-f347edbd7b10
- url
- https://medium.com/@ramitha33/development-process-of-the-java-ee-web-app-f347edbd7b10
- canonical_url
- https://medium.com/@ramitha33/development-process-of-the-java-ee-web-app-f347edbd7b10
- author_url
- https://medium.com/@ramitha33
- status
- ok
- fetched_at
- 2026-07-19 21:04:01