Skip to content

Commit e76efce

Browse files
author
gabriele.sisinna
committed
Refactor course module to ports and adapters architecture
1 parent 8597584 commit e76efce

24 files changed

Lines changed: 447 additions & 146 deletions

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ This project is built so you can learn, test, and modify small examples instead
55
Full repo learning notes are in [docs/README.md](./docs/README.md).
66
Deployment notes and production files are in [deploy/README.md](./deploy/README.md).
77

8+
## Software architecture
9+
10+
The repo now teaches two architectural styles:
11+
12+
- a simple layered flow for the student example
13+
- a ports-and-adapters style for the course example
14+
15+
Read [docs/architecture.md](./docs/architecture.md) for the design rationale and package boundaries.
16+
817
## What this repo teaches
918

1019
### Plain Java
@@ -50,10 +59,13 @@ Read these files in order:
5059
5. `src/main/java/com/example/demo/basics/generics/Box.java`
5160
6. `src/main/java/com/example/demo/basics/streams/StudentAnalytics.java`
5261
7. `src/main/java/com/example/demo/spring/service/StudentService.java`
53-
8. `src/main/java/com/example/demo/spring/persistence/service/CourseService.java`
54-
9. `src/main/java/com/example/demo/spring/persistence/controller/CourseController.java`
55-
10. `src/main/java/com/example/demo/spring/config/AppLearningProperties.java`
56-
11. `src/test/java/com/example/demo/spring/persistence/controller/CourseControllerTest.java`
62+
8. `src/main/java/com/example/demo/spring/persistence/domain/Course.java`
63+
9. `src/main/java/com/example/demo/spring/persistence/service/CourseService.java`
64+
10. `src/main/java/com/example/demo/spring/persistence/service/CourseApplicationService.java`
65+
11. `src/main/java/com/example/demo/spring/persistence/store/MongoCourseStore.java`
66+
12. `src/main/java/com/example/demo/spring/persistence/controller/CourseController.java`
67+
13. `src/main/java/com/example/demo/spring/config/AppLearningProperties.java`
68+
14. `src/test/java/com/example/demo/spring/persistence/controller/CourseControllerTest.java`
5769

5870
## Package guide
5971

@@ -80,7 +92,7 @@ Read these files in order:
8092
- `com.example.demo.spring.model`
8193
- request and response models
8294
- `com.example.demo.spring.persistence`
83-
- validation, MongoDB documents, JSON APIs, and security examples
95+
- validation, MongoDB documents, layered architecture, and security examples
8496
- `com.example.demo.spring.config`
8597
- OpenAPI config and typed configuration properties
8698

docs/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ This folder explains how to use the repository as a guided Java and Spring Boot
66

77
1. [Learning Path](./learning-path.md)
88
2. [Repo Map](./repo-map.md)
9-
3. [Java Basics](./java-basics.md)
10-
4. [Spring Boot](./spring-boot.md)
11-
5. [Testing](./testing.md)
12-
6. [Tooling](./tooling.md)
13-
7. [Deployment](./deployment.md)
9+
3. [Architecture](./architecture.md)
10+
4. [Java Basics](./java-basics.md)
11+
5. [Spring Boot](./spring-boot.md)
12+
6. [Testing](./testing.md)
13+
7. [Tooling](./tooling.md)
14+
8. [Deployment](./deployment.md)
1415

1516
## Main idea
1617

docs/architecture.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Architecture Guide
2+
3+
This repo uses two architectural levels on purpose:
4+
5+
- a simple layered flow for the student example
6+
- a more explicit ports-and-adapters flow for the course example
7+
8+
That split is intentional for learning. You can start with the simpler version and then see how a more maintainable design looks when persistence and security become more important.
9+
10+
## High-level structure
11+
12+
```text
13+
HTTP request
14+
-> controller
15+
-> use-case interface
16+
-> application service
17+
-> store interface
18+
-> Mongo adapter
19+
-> Spring Data repository
20+
-> MongoDB
21+
```
22+
23+
For responses, the flow goes back in the opposite direction, with the controller mapping domain objects into response DTOs.
24+
25+
## Why the course module is more structured
26+
27+
The student flow is intentionally small and direct so beginners can learn controller/service/repository basics first.
28+
29+
The course flow uses stronger boundaries because it is the better example for long-term maintainability:
30+
31+
- the controller should not know MongoDB details
32+
- the application service should not depend on web DTOs
33+
- the domain model should not depend on Spring Data classes
34+
- persistence-specific mapping should live in the adapter layer
35+
36+
This keeps each layer focused on one job.
37+
38+
## Main patterns used
39+
40+
### Layered architecture
41+
42+
The repo still follows a familiar layered shape:
43+
44+
- controller layer
45+
- service/application layer
46+
- persistence layer
47+
48+
That makes the code easy to navigate.
49+
50+
### Dependency inversion
51+
52+
The application service depends on `CourseStore`, not on `MongoRepository`.
53+
54+
That means the core use case depends on an interface, and the Mongo adapter implements it. This is the main architectural improvement in the repo.
55+
56+
### Ports and adapters
57+
58+
The course module uses a lightweight hexagonal style:
59+
60+
- input port: `CourseService`
61+
- output port: `CourseStore`
62+
- adapter: `MongoCourseStore`
63+
64+
This is useful because it makes framework code replaceable instead of central to the core logic.
65+
66+
### Repository pattern
67+
68+
`CourseRepository` is the Spring Data repository for MongoDB.
69+
70+
It is not used directly by the controller or application service anymore. It sits behind `MongoCourseStore`.
71+
72+
### Mapper pattern
73+
74+
There are two kinds of translation in the course module:
75+
76+
- controller maps request DTOs into application commands and domain objects into response DTOs
77+
- `CourseDocumentMapper` maps domain objects into Mongo documents and back
78+
79+
This avoids leaking transport or database concerns across layers.
80+
81+
## Package roles in the course module
82+
83+
- `spring.persistence.domain`
84+
- framework-independent domain model
85+
- `spring.persistence.service`
86+
- input port and application service
87+
- `spring.persistence.store`
88+
- output port and Mongo adapter
89+
- `spring.persistence.document`
90+
- MongoDB document classes
91+
- `spring.persistence.repository`
92+
- Spring Data repository interfaces
93+
- `spring.persistence.controller`
94+
- HTTP entry points and request/response mapping
95+
96+
## Why this is better than the previous version
97+
98+
Before the refactor:
99+
100+
- the service knew about request DTOs
101+
- the service returned response DTOs
102+
- the service depended directly on the Mongo repository
103+
104+
After the refactor:
105+
106+
- the controller owns HTTP DTO mapping
107+
- the service owns use-case logic
108+
- the store adapter owns persistence translation
109+
- the domain model stays free of Spring and Mongo classes
110+
111+
That is a cleaner separation of responsibilities.
112+
113+
## Tradeoff
114+
115+
This architecture adds more files.
116+
117+
That is the cost of clearer boundaries.
118+
119+
For a tiny project, that would sometimes be too much. For a learning repo, it is useful because it shows what "better architecture" actually looks like in code rather than only describing it in theory.

docs/learning-path.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,23 @@ Learn:
7373
Read:
7474

7575
- `spring/persistence/model/CreateCourseRequest.java`
76+
- `spring/persistence/domain/Course.java`
7677
- `spring/persistence/document/CourseDocument.java`
7778
- `spring/persistence/repository/CourseRepository.java`
79+
- `spring/persistence/store/CourseStore.java`
80+
- `spring/persistence/store/MongoCourseStore.java`
7881
- `spring/persistence/config/CourseDataInitializer.java`
7982
- `spring/persistence/service/CourseService.java`
83+
- `spring/persistence/service/CourseApplicationService.java`
8084
- `spring/persistence/controller/CourseController.java`
8185
- `spring/security/SecurityConfig.java`
8286

8387
Learn:
8488

8589
- validation with `@Valid`
8690
- constraints like `@NotBlank` and `@Min`
91+
- domain model vs persistence model
92+
- input port vs output port
8793
- MongoDB document mapping
8894
- JSON request/response flow
8995
- basic auth with Spring Security

docs/repo-map.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ This page explains why each package exists.
3939

4040
## Spring persistence packages
4141

42+
- `spring.persistence.domain`
43+
- framework-independent business model
4244
- `spring.persistence.document`
4345
- MongoDB document classes
4446
- `spring.persistence.repository`
45-
- Spring Data repository interfaces
47+
- Spring Data repository interfaces used only by the adapter
4648
- `spring.persistence.config`
4749
- seed data and persistence bootstrap
4850
- `spring.persistence.service`
49-
- business logic using the repository layer
51+
- input port and application service
52+
- `spring.persistence.store`
53+
- output port plus MongoDB adapter and mapper
5054
- `spring.persistence.controller`
5155
- secured HTTP endpoints
5256
- `spring.persistence.model`

docs/spring-boot.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This repo contains two Spring learning tracks:
44

55
- a simple in-memory student API
6-
- a more realistic secured course API with validation and persistence
6+
- a more realistic secured course API with validation, persistence, and explicit architectural boundaries
77

88
## Spring Boot structure
99

@@ -19,6 +19,14 @@ This repo shows both:
1919
- simple flow with in-memory data
2020
- document-backed flow with MongoDB
2121

22+
The course module also shows a stronger architecture:
23+
24+
- controller
25+
- use-case interface
26+
- application service
27+
- store interface
28+
- MongoDB adapter
29+
2230
## Controllers
2331

2432
Examples:
@@ -46,6 +54,7 @@ Examples:
4654

4755
- `StudentService`
4856
- `CourseService`
57+
- `CourseApplicationService`
4958

5059
Service responsibilities:
5160

@@ -70,6 +79,7 @@ Why it matters:
7079
- easier testing
7180
- clearer dependencies
7281
- better separation of responsibilities
82+
- support for dependency inversion through interfaces
7383

7484
## Request and response models
7585

@@ -138,15 +148,21 @@ Handled cases include:
138148

139149
Main files:
140150

151+
- `Course`
141152
- `CourseDocument`
142153
- `CourseRepository`
154+
- `CourseStore`
155+
- `MongoCourseStore`
143156
- `application.yml`
144157
- `CourseDataInitializer`
145158

146159
Key ideas:
147160

161+
- `Course` is the framework-independent domain model
148162
- `@Document` maps a class to a MongoDB collection
149163
- a `MongoRepository` gives CRUD operations
164+
- `CourseStore` is the output port used by the application service
165+
- `MongoCourseStore` is the adapter that hides Spring Data details
150166
- `CourseLevel` enum keeps the `level` field restricted to valid values
151167
- MongoDB stores JSON-like documents instead of rows
152168
- `CourseDataInitializer` seeds example documents at startup

docs/testing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ What they teach:
3333

3434
- behavior-focused assertions
3535
- testing single classes
36-
- mocking with Mockito in `CourseServiceTest`
36+
- mocking ports and adapters with Mockito in `CourseServiceTest`
3737

3838
## Configuration tests
3939

@@ -65,6 +65,7 @@ What they teach:
6565
- JSON assertions
6666
- authentication testing with `httpBasic(...)`
6767
- documentation endpoint checks
68+
- controller testing against a use-case interface instead of a concrete implementation
6869

6970
## How to read a test well
7071

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.example.demo.spring.persistence.config;
22

3-
import com.example.demo.spring.persistence.document.CourseDocument;
3+
import com.example.demo.spring.persistence.domain.Course;
44
import com.example.demo.spring.persistence.model.CourseLevel;
5-
import com.example.demo.spring.persistence.repository.CourseRepository;
5+
import com.example.demo.spring.persistence.store.CourseStore;
66
import org.springframework.boot.CommandLineRunner;
77
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
88
import org.springframework.context.annotation.Bean;
@@ -14,14 +14,14 @@ public class CourseDataInitializer {
1414

1515
@Bean
1616
@ConditionalOnProperty(name = "app.courses.seed.enabled", havingValue = "true", matchIfMissing = true)
17-
CommandLineRunner seedCourses(CourseRepository courseRepository) {
17+
CommandLineRunner seedCourses(CourseStore courseStore) {
1818
return args -> {
19-
if (courseRepository.count() > 0) {
19+
if (!courseStore.isEmpty()) {
2020
return;
2121
}
2222

23-
courseRepository.save(new CourseDocument("Java Generics Deep Dive", CourseLevel.INTERMEDIATE, 6, true));
24-
courseRepository.save(new CourseDocument("Spring Boot REST APIs", CourseLevel.BEGINNER, 5, true));
23+
courseStore.save(new Course(null, "Java Generics Deep Dive", CourseLevel.INTERMEDIATE, 6, true));
24+
courseStore.save(new Course(null, "Spring Boot REST APIs", CourseLevel.BEGINNER, 5, true));
2525
};
2626
}
2727
}

0 commit comments

Comments
 (0)