Skip to content

Commit 793a080

Browse files
feat: Project setup (#1)
1 parent c4bf5c4 commit 793a080

File tree

65 files changed

+3201
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3201
-2
lines changed

.github/workflows/gradle-build.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Gradle Build
2+
3+
on:
4+
push:
5+
branches:
6+
- main # Runs on pushes to main branch
7+
pull_request:
8+
branches:
9+
- main # Runs on pull requests for merging into main
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v5
17+
18+
- name: Setup Java
19+
uses: actions/setup-java@v5
20+
with:
21+
distribution: 'temurin'
22+
java-version: 21
23+
24+
- name: Use gradle/actions/setup-gradle@v4.3.0
25+
uses: gradle/actions/setup-gradle@v4.4.2
26+
27+
- name: Make gradlew executable
28+
run: chmod +x ./gradlew
29+
30+
- name: Build with Gradle
31+
run: ./gradlew build

.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/

README.md

Lines changed: 311 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,311 @@
1-
# spring-boot-java-structured-concurrency
2-
Spring Boot Java multi-project Gradle build sample using Structured Concurrency
1+
# Spring Boot + Java + Structured Concurrency
2+
3+
This is a sample project using
4+
[Spring Boot](https://github.com/spring-projects/spring-boot),
5+
[Java](https://www.java.com)
6+
and
7+
[Structured Concurrency](https://docs.oracle.com/en/java/javase/21/core/structured-concurrency.html)
8+
to optimize performance when executing multiple requests
9+
to one or more remote services. This is useful in the case where the
10+
data from one request is not required to continue the next request.
11+
12+
## Scenario
13+
14+
Imagine you are building a service which needs to present traveling/vacation offers to a client.
15+
This could for example include offers such as:
16+
- Flights
17+
- Hotels
18+
- Rental cars
19+
20+
You find a third-party service provider for each type of offer, but you are forced
21+
to send 3 requests to fetch all the relevant offers before returning it to the client.
22+
The issue here is that with synchronous code, you would have to execute one
23+
request at a time which could result in a slow response time.
24+
25+
## Services
26+
27+
### Provider REST API
28+
The **provider** subproject is independently runnable and will spin up a Spring Boot REST API.
29+
This service includes the following endpoints:
30+
- `GET /flights`
31+
- `GET /hotels`
32+
- `GET /rentalcars`
33+
34+
Each endpoint, will return the full list of available entities from the database.
35+
An artificial delay of 2000 milliseconds has been implemented for each endpoint.
36+
The purpose of this is to showcase the performance benefits when
37+
correctly using Structured Concurrency.
38+
39+
The **provider** subproject implements both the **model** and **persistence** subprojects.
40+
It can interact with an in-memory [H2database](https://github.com/h2database/h2database)
41+
using [Spring Data JPA](https://docs.spring.io/spring-data/jpa/reference/index.html).
42+
Additionally, it uses [Liquibase](https://github.com/liquibase/liquibase)
43+
for database changelogs, where dummy data has been added to the database.
44+
45+
### Gateway REST API
46+
The **gateway** subproject is independently runnable and will spin up a Spring Boot REST API.
47+
This service includes the following endpoints:
48+
- `GET /travel/details/async`
49+
- `GET /travel/details/sync`
50+
51+
For this project, we use Spring Boot and Structured Concurrency
52+
so we can achieve optimized performance.
53+
Structured Concurrency is a modern Java feature
54+
that allows us to manage multiple concurrent
55+
tasks in a more organized way.
56+
57+
The code below from [TravelService](/apps/gateway/src/main/java/com/github/thorlauridsen/service/TravelService.java)
58+
showcases how to use Structured Concurrency to execute 3 requests simultaneously.
59+
60+
```java
61+
public TravelDetails getAsync() throws InterruptedException {
62+
try (val scope = new StructuredTaskScope.ShutdownOnFailure()) {
63+
64+
val flightsTask = scope.fork(() -> fetchList("/flights", Flight.class));
65+
val hotelsTask = scope.fork(() -> fetchList("/hotels", Hotel.class));
66+
val carsTask = scope.fork(() -> fetchList("/rentalcars", RentalCar.class));
67+
68+
scope.join();
69+
scope.throwIfFailed(
70+
cause -> new IllegalStateException("Failed to fetch travel details", cause)
71+
);
72+
73+
return new TravelDetails(
74+
flightsTask.get(),
75+
hotelsTask.get(),
76+
carsTask.get()
77+
);
78+
}
79+
}
80+
```
81+
82+
The benefit is that we do not have to wait for one request to finish
83+
before starting the next request. The combined response time will be
84+
approximately the same duration as the slowest of the three requests.
85+
86+
You can see an example of how the data is
87+
fetched synchronously in the code below:
88+
```java
89+
public TravelDetails getSync() {
90+
val flights = fetchList("/flights", Flight.class);
91+
val hotels = fetchList("/hotels", Hotel.class);
92+
val rentalCars = fetchList("/rentalcars", RentalCar.class);
93+
94+
return new TravelDetails(flights, hotels, rentalCars);
95+
}
96+
```
97+
This separate function has been added to showcase the differences
98+
in performance when running synchronous and asynchronous code.
99+
Example logs can be seen below:
100+
101+
```
102+
14:16:45.736 : Fetching travel details synchronously from http://localhost:8081
103+
14:16:45.736 : Executing request HTTP GET /hotels
104+
14:16:48.413 : Executing request HTTP GET /flights
105+
14:16:50.468 : Executing request HTTP GET /rentalcars
106+
14:16:52.513 : Fetched travel details in 6776 ms
107+
14:16:56.217 : Fetching travel details asynchronously from http://localhost:8081
108+
14:16:56.225 : Executing request HTTP GET /hotels
109+
14:16:56.227 : Executing request HTTP GET /flights
110+
14:16:56.229 : Executing request HTTP GET /rentalcars
111+
14:16:58.257 : Fetched travel details in 2041 ms
112+
```
113+
114+
When fetching data from **n** independent external services:
115+
116+
#### Total execution time
117+
- **Synchronous code**: Sum of individual request times `T_sync = t₁ + t₂ + ... + tₙ`
118+
- **Asynchronous code**: Duration of the slowest request `T_async = max(t₁, t₂, ..., tₙ)`
119+
120+
## Usage
121+
Clone the project to your local machine, go to the root directory and use
122+
these two commands in separate terminals.
123+
```
124+
./gradlew gateway:bootRun
125+
```
126+
```
127+
./gradlew provider:bootRun
128+
```
129+
The provider service will be running with an in-memory H2 database.
130+
You can also use IntelliJ IDEA to easily run the two services at once.
131+
132+
### Docker Compose
133+
To run the project with [Docker Compose](https://docs.docker.com/compose/), go to the root directory and use:
134+
```
135+
docker-compose up -d
136+
```
137+
This will run the two services at once where the provider service is using a PostgreSQL database.
138+
139+
### Swagger Documentation
140+
Once both services is running, you can navigate to http://localhost:8080/
141+
and http://localhost:8081/ to view the Swagger documentation for each service.
142+
143+
## Technology
144+
- [JDK21](https://openjdk.org/projects/jdk/21/) - Latest JDK with long-term support
145+
- [Gradle](https://github.com/gradle/gradle) - Used for compilation, building, testing and dependency management
146+
- [Spring Boot Web MVC](https://github.com/spring-projects/spring-boot) - For creating REST APIs
147+
- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/reference/index.html) - Repository support for JPA
148+
- [Springdoc](https://github.com/springdoc/springdoc-openapi) - Provides Swagger documentation for REST APIs
149+
- [PostgreSQL](https://www.postgresql.org/) - Open-source relational database
150+
- [H2database](https://github.com/h2database/h2database) - Provides an in-memory database for simple local testing
151+
- [Liquibase](https://github.com/liquibase/liquibase) - Used to manage database schema changelogs
152+
- [WireMock](https://github.com/wiremock/wiremock) - For mocking HTTP services in tests
153+
- [Lombok](https://github.com/projectlombok/lombok) - Used to reduce boilerplate code
154+
155+
## Gradle best practices for Kotlin
156+
[docs.gradle.org](https://docs.gradle.org/current/userguide/performance.html) - [kotlinlang.org](https://kotlinlang.org/docs/gradle-best-practices.html)
157+
158+
### Preface
159+
This project uses Java but the linked article above is generally meant
160+
for Kotlin projects. However, I still think that the recommended best
161+
practices for Gradle are relevant for a Java project as well.
162+
The recommendations can be useful for all sorts of Gradle projects.
163+
164+
### ✅ Use Kotlin DSL
165+
This project uses Kotlin DSL instead of the traditional Groovy DSL by
166+
using **build.gradle.kts** files instead of **build.gradle** files.
167+
This gives us the benefits of strict typing which lets IDEs provide
168+
better support for refactoring and auto-completion.
169+
If you want to read more about the benefits of using
170+
Kotlin DSL over Groovy DSL, you can check out
171+
[gradle-kotlin-dsl-vs-groovy-dsl](https://github.com/thorlauridsen/gradle-kotlin-dsl-vs-groovy-dsl)
172+
173+
### ✅ Use a version catalog
174+
175+
This project uses a version catalog
176+
[local.versions.toml](gradle/local.versions.toml)
177+
which allows us to centralize dependency management.
178+
We can define versions, libraries, bundles and plugins here.
179+
This enables us to use Gradle dependencies consistently across the entire project.
180+
181+
Dependencies can then be implemented in a specific **build.gradle.kts** file as such:
182+
```kotlin
183+
implementation(local.spring.boot.starter)
184+
```
185+
186+
The Kotlinlang article says to name the version catalog **libs.versions.toml**
187+
but for this project it has been named **local.versions.toml**. The reason
188+
for this is that we can create a shared common version catalog which can
189+
be used across Gradle projects. Imagine that you are working on multiple
190+
similar Gradle projects with different purposes, but each project has some
191+
specific dependencies but also some dependencies in common. The dependencies
192+
that are common across projects could be placed in the shared version catalog
193+
while specific dependencies are placed in the local version catalog.
194+
195+
### ✅ Use local build cache
196+
197+
This project uses a local
198+
[build cache](https://docs.gradle.org/current/userguide/build_cache.html)
199+
for Gradle which is a way to increase build performance because it will
200+
re-use outputs produced by previous builds. It will store build outputs
201+
locally and allow subsequent builds to fetch these outputs from the cache
202+
when it knows that the inputs have not changed.
203+
This means we can save time building
204+
205+
Gradle build cache is disabled by default so it has been enabled for this
206+
project by updating the root [gradle.properties](gradle.properties) file:
207+
```properties
208+
org.gradle.caching=true
209+
```
210+
211+
This is enough to enable the local build cache
212+
and by default, this will use a directory in the Gradle User Home
213+
to store build cache artifacts.
214+
215+
### ✅ Use configuration cache
216+
217+
This project uses
218+
[Gradle configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html)
219+
and this will improve build performance by caching the result of the
220+
configuration phase and reusing this for subsequent builds. This means
221+
that Gradle tasks can be executed faster if nothing has been changed
222+
that affects the build configuration. If you update a **build.gradle.kts**
223+
file, the build configuration has been affected.
224+
225+
This is not enabled by default, so it is enabled by defining this in
226+
the root [gradle.properties](gradle.properties) file:
227+
```properties
228+
org.gradle.configuration-cache=true
229+
org.gradle.configuration-cache.parallel=true
230+
```
231+
232+
### ✅ Use modularization
233+
234+
This project uses modularization to create a
235+
[multi-project Gradle build](https://docs.gradle.org/current/userguide/multi_project_builds.html).
236+
The benefit here is that we optimize build performance and structure our
237+
entire project in a meaningful way. This is more scalable as it is easier
238+
to grow a large project when you structure the code like this.
239+
240+
```
241+
root
242+
│─ build.gradle.kts
243+
│─ settings.gradle.kts
244+
│─ apps
245+
│ └─ gateway
246+
│ └─ build.gradle.kts
247+
│ └─ provider
248+
│ └─ build.gradle.kts
249+
│─ modules
250+
│ ├─ model
251+
│ │ └─ build.gradle.kts
252+
│ └─ persistence
253+
│ └─ build.gradle.kts
254+
```
255+
256+
This also allows us to specifically decide which Gradle dependencies will be used
257+
for which subproject. Each subproject should only use exactly the dependencies
258+
that they need.
259+
260+
Subprojects located under [apps](apps) are runnable, so this means we can
261+
run the **gateway** or **provider** project to spin up a Spring Boot REST API.
262+
We can add more subprojects under [apps](apps) to create additional
263+
runnable microservices.
264+
265+
Subprojects located under [modules](modules) are not independently runnable.
266+
The subprojects are used to structure code into various layers. The **model**
267+
subproject is the most inner layer and contains domain model classes and this
268+
subproject knows nothing about any of the other subprojects. The purpose of
269+
the **persistence** subproject is to manage the code responsible for
270+
interacting with the database. We can add more non-runnable subprojects
271+
under [modules](modules) if necessary. This could for example
272+
be a third-party integration.
273+
274+
---
275+
276+
#### Subproject with other subproject as dependency
277+
278+
The subprojects in this repository may use other subprojects as dependencies.
279+
280+
In our root [settings.gradle.kts](settings.gradle.kts) we have added:
281+
```kotlin
282+
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
283+
```
284+
Which allows us to add a subproject as a dependency in another subproject:
285+
286+
```kotlin
287+
dependencies {
288+
implementation(projects.model)
289+
}
290+
```
291+
292+
This essentially allows us to define this structure:
293+
294+
```
295+
gateway
296+
└─ model
297+
298+
provider
299+
│─ model
300+
└─ persistence
301+
302+
persistence
303+
└─ model
304+
305+
model has no dependencies
306+
```
307+
308+
## Meta
309+
310+
This project has been created with the sample code structure from
311+
[thorlauridsen/spring-boot-java-sample](https://github.com/thorlauridsen/spring-boot-java-sample)

0 commit comments

Comments
 (0)