In the previous tutorial, I introduced to you the basic ideas of Clean Architecture. In this tutorial, I will go into detail about how to implement Clean Architecture with a Java application!
For easy understanding, I will take the student management application example mentioned in part 1 to implement follow the Clean Architecture as follows:
This is the Maven project with many modules.
Module entities
You can see, we have the entities module to define student information:
For simplicity, I just define 2 basic information of a student in the Student class as follows:
1 2 3 4 |
package com.huongdanjava.cleanarchitecture.entities; public record Student(String name, int age) { } |
And in this module’s pom.xml file, I don’t declare any library or framework.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>entities</artifactId> </project> |
Module use-cases
For the use-cases module, for simplicity, I only define one use case, which is to search for student information by name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.huongdanjava.cleanarchitecture.usecases.student; import com.huongdanjava.cleanarchitecture.entities.Student; import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter; public class FindStudentByNameUseCase { private StudentAdapter adapter; public FindStudentByNameUseCase(StudentAdapter adapter) { this.adapter = adapter; } public Student find(String name) { return adapter.findByName(name); } } |
Here, as you can see, I have defined an additional package as adapter. In the idea of Clean Architecture, the adapter layer will be outside the use-cases layer, but here, we can include this adapter layer in the use-cases module, no need to add an adapter module to define interfaces, not necessarily. But if you want to closely follow the idea of Clean Architecture, you can introduce more adapter module too.
StudentAdapter has the following contents:
1 2 3 4 5 6 7 8 |
package com.huongdanjava.cleanarchitecture.usecases.adapter; import com.huongdanjava.cleanarchitecture.entities.Student; public interface StudentAdapter { Student findByName(String name); } |
The content of the pom.xml file in the use-cases module, I also don’t have a library, or framework at all, just a dependency on the entities module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>use-cases</artifactId> <dependencies> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>entities</artifactId> </dependency> </dependencies> </project> |
Module db
We will implement the student information retrieval in the db module.
Here, I will use spring-data-jpa to do database manipulation!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>db</artifactId> <dependencies> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>use-cases</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> </dependency> </dependencies> </project> |
As you can see, I also added Hibernate dependency for the implementation part of JPA.
StudentModel has the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package com.huongdanjava.cleanarchitecture.db.model; import java.io.Serializable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; @Table(name = "student") @Entity public class StudentModel implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private Long id; @Column private String name; @Column private int age; 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; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } |
StudentRepository has the following content:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.huongdanjava.cleanarchitecture.db; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.huongdanjava.cleanarchitecture.db.model.StudentModel; @Repository public interface StudentRepository extends JpaRepository<StudentModel, Long> { StudentModel findByName(String name); } |
As you can see here, I define a query method that allows us to get student information from the student’s name.
And now we can implement StudentAdapter in the db module as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.huongdanjava.cleanarchitecture.db; import org.springframework.beans.factory.annotation.Autowired; import com.huongdanjava.cleanarchitecture.db.mapper.StudentMapper; import com.huongdanjava.cleanarchitecture.db.model.StudentModel; import com.huongdanjava.cleanarchitecture.entities.Student; import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter; public class StudentAdapterImpl implements StudentAdapter { @Autowired private StudentRepository studentRepository; @Override public Student findByName(String name) { StudentModel findByName = studentRepository.findByName(name); return StudentMapper.toEntity(findByName); } } |
Here, as you can see, I have added a class StudentMapper to convert data from database to entity, and then, if this entity is used somewhere, for example module rest, we will have another Mapper class, to convert from the entity to the dto of the rest to return it to the user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.huongdanjava.cleanarchitecture.db.mapper; import com.huongdanjava.cleanarchitecture.db.model.StudentModel; import com.huongdanjava.cleanarchitecture.entities.Student; public class StudentMapper { public static Student toEntity(StudentModel model) { if (model == null) { return null; } Student student = new Student(model.getName(), model.getAge()); return student; } } |
Using this Mapper class will help us reduce dependencies between modules, we can easily add new or remove modules that we will be using for the application, with minimal code changes.
Module rest
After retrieving data from the database, now is the time to implement the rest module, taking on the role of exposing API for users to use.
The rest module’s pom.xml file has the following contents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>rest</artifactId> <dependencies> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>use-cases</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> </dependencies> </project> |
I use spring-web dependency to define RESTful APIs, use-cases dependency to call application use cases.
StudentDto has the following content:
1 2 3 4 5 |
package com.huongdanjava.cleanarchitecture.rest.dto; public record StudentDto(String name, int age) { } |
StudentMapper has the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.huongdanjava.cleanarchitecture.rest.mapper; import com.huongdanjava.cleanarchitecture.entities.Student; import com.huongdanjava.cleanarchitecture.rest.dto.StudentDto; public class StudentMapper { public static StudentDto toDto(Student entity) { if (entity == null) { return null; } StudentDto studentDto = new StudentDto(entity.name(), entity.age()); return studentDto; } } |
And the StudentController expose API class gets student information by name with the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.huongdanjava.cleanarchitecture.rest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.huongdanjava.cleanarchitecture.entities.Student; import com.huongdanjava.cleanarchitecture.rest.dto.StudentDto; import com.huongdanjava.cleanarchitecture.rest.mapper.StudentMapper; import com.huongdanjava.cleanarchitecture.usecases.student.FindStudentByNameUseCase; @RestController @RequestMapping("/student") public class StudentController { @Autowired private FindStudentByNameUseCase findStudentByNameUseCase; @GetMapping("/find") public ResponseEntity<StudentDto> findByName(@RequestParam String name) { Student student = findStudentByNameUseCase.find(name); return new ResponseEntity<>(StudentMapper.toDto(student), HttpStatus.OK); } } |
Here, I am auto wiring the FindStudentByNameUseCase because I am taking advantage of the benefit of this application with the Spring framework, we will define use cases in the Spring container. If your application uses other frameworks then the use of the use cases will depend on those frameworks.
Module configuration
As I said, in order for the application to run, we need to have the configuration module.
I am using Spring Boot to run the application:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.huongdanjava.cleanarchitecture; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
and define the use case FindStudentByNameUseCase in the UseCaseConfiguration class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.huongdanjava.cleanarchitecture.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.huongdanjava.cleanarchitecture.db.StudentAdapterImpl; import com.huongdanjava.cleanarchitecture.usecases.adapter.StudentAdapter; import com.huongdanjava.cleanarchitecture.usecases.student.FindStudentByNameUseCase; @Configuration public class UseCaseConfiguration { @Bean public FindStudentByNameUseCase findStudentByNameUseCase(StudentAdapter studentAdapter) { return new FindStudentByNameUseCase(studentAdapter); } @Bean public StudentAdapter studentAdapter() { return new StudentAdapterImpl(); } } |
The content of pom.xml of the configuration module will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>configuration</artifactId> <dependencies> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>use-cases</artifactId> </dependency> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>db</artifactId> </dependency> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>coom.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
If you have noticed, I have defined the most generic rest and db modules possible, and the application configuration in the configuration module will determine how our application runs! For example, here, I am using MySQL to run the application, later if I want to switch to another database system like PostgreSQL, for example, what I need to do is just change this configuration module, …
The file pom.xml of the parent project has the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.huongdanjava</groupId> <artifactId>clean-architecture-example</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>21</java.version> </properties> <modules> <module>rest</module> <module>use-cases</module> <module>db</module> <module>entities</module> <module>configuration</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>entities</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>use-cases</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>db</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.huongdanjava</groupId> <artifactId>rest</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>6.5.2.Final</version> </dependency> </dependencies> </dependencyManagement> </project> |
At this point, we have completed our example application.
Suppose I have a student table and data is created in the MySQL database server as follows:
1 2 3 4 5 6 7 8 |
CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` bigint(2) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; INSERT INTO student SET name='Khanh', age=33; |
then when running the application and requesting to http://localhost:9090/student/find?name=Khanh, you will see the results as follows: