Skip to content

Commit 5e893cd

Browse files
authored
BAEL-9307: cqrs with spring modulith (#18700)
* BAEL-9307: code samples * BAEL-9307: docker compose * BAEL-9307: repackage * BAEL-9307: minor refactoring * BAEL-9307: repackage * BAEL-9307: remove sysout * BAEL-9307: changes * BAEL-9307: pom changes and table schema * BAEL-9307: simplify * BAEL-9307: version param * BAEL-9307: extract method * BAEL-9307: code review * BAEL-9307: code review * BAEL-9307: simplify * BAEL-9307: code review * BAEL-9307: upgrade lib versions
1 parent 6256b2d commit 5e893cd

23 files changed

+680
-4
lines changed

spring-boot-modules/spring-boot-libraries-3/pom.xml

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@
6464
<version>${spring-modulith.version}</version>
6565
<scope>test</scope>
6666
</dependency>
67+
<dependency>
68+
<groupId>org.springframework.modulith</groupId>
69+
<artifactId>spring-modulith-core</artifactId>
70+
<version>${spring-modulith.version}</version>
71+
</dependency>
72+
73+
<dependency>
74+
<groupId>org.jmolecules</groupId>
75+
<artifactId>jmolecules-cqrs-architecture</artifactId>
76+
<version>${jmolecules-cqrs-architecture.version}</version>
77+
</dependency>
6778

6879
<dependency>
6980
<groupId>org.springframework.boot</groupId>
@@ -123,19 +134,36 @@
123134
<artifactId>jolokia-support-spring</artifactId>
124135
<version>${jolokia-support-spring.version}</version>
125136
</dependency>
137+
<dependency>
138+
<groupId>ch.qos.logback</groupId>
139+
<artifactId>logback-classic</artifactId>
140+
<version>${logback-core.version}</version>
141+
</dependency>
142+
<dependency>
143+
<groupId>ch.qos.logback</groupId>
144+
<artifactId>logback-core</artifactId>
145+
<version>${logback-core.version}</version>
146+
</dependency>
147+
<dependency>
148+
<groupId>net.logstash.logback</groupId>
149+
<artifactId>logstash-logback-encoder</artifactId>
150+
<version>8.1</version>
151+
</dependency>
126152
</dependencies>
127153

128154

129155
<properties>
130-
<spring-boot.version>3.1.5</spring-boot.version>
131-
<spring-modulith.version>1.1.3</spring-modulith.version>
156+
<spring-boot.version>3.5.4</spring-boot.version>
157+
<spring-modulith.version>1.4.2</spring-modulith.version>
132158
<testcontainers.version>1.19.3</testcontainers.version>
133159
<awaitility.version>4.2.0</awaitility.version>
134160
<postgresql.version>42.3.1</postgresql.version>
135161
<h2.version>2.2.224</h2.version>
136162
<jolokia-support-spring.version>2.2.1</jolokia-support-spring.version>
137163
<eventuate.tram.version>0.36.0.RELEASE</eventuate.tram.version>
138164
<json-unit-assertj.version>3.5.0</json-unit-assertj.version>
165+
<jmolecules-cqrs-architecture.version>1.10.0</jmolecules-cqrs-architecture.version>
166+
<logback-core.version>1.5.18</logback-core.version>
139167
</properties>
140168

141169
<build>

spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/Application.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55

66
/**
7-
* use the appropriate profile: eventuate|modulith
7+
* use the appropriate profile: eventuate|modulith|cqrs
88
*/
99
@SpringBootApplication
1010
public class Application {
1111

1212
public static void main(String[] args) {
13-
SpringApplication.run( Application.class, args);
13+
SpringApplication.run(Application.class, args);
1414
}
1515

1616
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import java.time.Instant;
4+
import java.util.List;
5+
6+
import org.jmolecules.architecture.cqrs.QueryModel;
7+
8+
@QueryModel
9+
record AvailableMovieSeats(String title, String screenRoom, Instant startTime, List<String> freeSeats) {
10+
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import java.time.Instant;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.stream.IntStream;
7+
8+
import jakarta.persistence.CollectionTable;
9+
import jakarta.persistence.Column;
10+
import jakarta.persistence.ElementCollection;
11+
import jakarta.persistence.Entity;
12+
import jakarta.persistence.GeneratedValue;
13+
import jakarta.persistence.Id;
14+
import jakarta.persistence.JoinColumn;
15+
16+
@Entity
17+
class Movie {
18+
19+
@Id
20+
@GeneratedValue
21+
private Long id;
22+
private String title;
23+
private String screenRoom;
24+
private Instant startTime;
25+
26+
@ElementCollection
27+
@CollectionTable(name = "screen_room_free_seats", joinColumns = @JoinColumn(name = "room_id"))
28+
@Column(name = "seat_number")
29+
private List<String> freeSeats = allSeats();
30+
31+
@ElementCollection
32+
@CollectionTable(name = "screen_room_occupied_seats", joinColumns = @JoinColumn(name = "room_id"))
33+
@Column(name = "seat_number")
34+
private List<String> occupiedSeats = new ArrayList<>();
35+
36+
Movie(String movieName, String screenRoom, Instant startTime) {
37+
this.title = movieName;
38+
this.screenRoom = screenRoom;
39+
this.startTime = startTime;
40+
}
41+
42+
void occupySeat(String seatNumber) {
43+
if (freeSeats.contains(seatNumber)) {
44+
freeSeats.remove(seatNumber);
45+
occupiedSeats.add(seatNumber);
46+
} else {
47+
throw new IllegalArgumentException("Seat " + seatNumber + " is not available.");
48+
}
49+
}
50+
51+
void freeSeat(String seatNumber) {
52+
if (occupiedSeats.contains(seatNumber)) {
53+
occupiedSeats.remove(seatNumber);
54+
freeSeats.add(seatNumber);
55+
} else {
56+
throw new IllegalArgumentException("Seat " + seatNumber + " is not currently occupied.");
57+
}
58+
}
59+
60+
static List<String> allSeats() {
61+
List<Integer> rows = IntStream.range(1, 20)
62+
.boxed()
63+
.toList();
64+
65+
return IntStream.rangeClosed('A', 'J')
66+
.mapToObj(c -> String.valueOf((char) c))
67+
.flatMap(col -> rows.stream()
68+
.map(row -> col + row))
69+
.sorted()
70+
.toList();
71+
}
72+
73+
protected Movie() {
74+
// Default constructor for JPA
75+
}
76+
77+
Instant startTime() {
78+
return startTime;
79+
}
80+
81+
String title() {
82+
return title;
83+
}
84+
85+
String screenRoom() {
86+
return screenRoom;
87+
}
88+
89+
List<String> freeSeats() {
90+
return List.copyOf(freeSeats);
91+
}
92+
93+
List<String> occupiedSeatsSeats() {
94+
return List.copyOf(freeSeats);
95+
}
96+
}
97+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import static java.time.Instant.now;
4+
import static java.time.temporal.ChronoUnit.DAYS;
5+
6+
import java.time.Instant;
7+
import java.util.List;
8+
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RequestParam;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequestMapping("/api/movies")
18+
class MovieController {
19+
20+
private final MovieRepository movieScreens;
21+
22+
MovieController(MovieRepository screenRooms) {
23+
this.movieScreens = screenRooms;
24+
}
25+
26+
/*
27+
curl -X GET "http://localhost:8080/api/seating/movies?range=week"
28+
*/
29+
@GetMapping
30+
List<UpcomingMovies> moviesToday(@RequestParam String range) {
31+
Instant endTime = endTime(range);
32+
return movieScreens.findUpcomingMoviesByStartTimeBetween(now(), endTime.truncatedTo(DAYS));
33+
}
34+
35+
/*
36+
curl -X GET http://localhost:8080/api/movies/1/seats
37+
*/
38+
@GetMapping("/{movieId}/seats")
39+
ResponseEntity<AvailableMovieSeats> movieSeating(@PathVariable Long movieId) {
40+
return ResponseEntity.of(movieScreens.findAvailableSeatsByMovieId(movieId));
41+
}
42+
43+
private static Instant endTime(String range) {
44+
return switch (range) {
45+
case "day" -> now().plus(1, DAYS);
46+
case "week" -> now().plus(7, DAYS);
47+
case "month" -> now().plus(30, DAYS);
48+
default -> throw new IllegalArgumentException("Invalid range: " + range);
49+
};
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import java.time.Instant;
4+
import java.util.List;
5+
import java.util.Optional;
6+
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
interface MovieRepository extends JpaRepository<Movie, Long> {
10+
11+
List<UpcomingMovies> findUpcomingMoviesByStartTimeBetween(Instant start, Instant end);
12+
13+
default Optional<AvailableMovieSeats> findAvailableSeatsByMovieId(Long movieId) {
14+
return findById(movieId).map(movie -> new AvailableMovieSeats(movie.title(), movie.screenRoom(), movie.startTime(), movie.freeSeats()));
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import static java.time.temporal.ChronoUnit.HOURS;
4+
5+
import java.time.Instant;
6+
import java.util.List;
7+
import java.util.stream.LongStream;
8+
9+
import org.jmolecules.event.annotation.DomainEventHandler;
10+
import org.springframework.boot.context.event.ApplicationReadyEvent;
11+
import org.springframework.context.event.EventListener;
12+
import org.springframework.modulith.events.ApplicationModuleListener;
13+
import org.springframework.stereotype.Component;
14+
15+
import com.baeldung.spring.modulith.cqrs.ticket.BookingCancelled;
16+
import com.baeldung.spring.modulith.cqrs.ticket.BookingCreated;
17+
18+
@Component
19+
class TicketBookingEventHandler {
20+
21+
private final MovieRepository screenRooms;
22+
23+
TicketBookingEventHandler(MovieRepository screenRooms) {
24+
this.screenRooms = screenRooms;
25+
}
26+
27+
@DomainEventHandler
28+
@ApplicationModuleListener
29+
void handleTicketBooked(BookingCreated booking) {
30+
Movie room = screenRooms.findById(booking.movieId())
31+
.orElseThrow();
32+
33+
room.occupySeat(booking.seatNumber());
34+
screenRooms.save(room);
35+
}
36+
37+
@DomainEventHandler
38+
@ApplicationModuleListener
39+
void handleTicketCancelled(BookingCancelled cancellation) {
40+
Movie room = screenRooms.findById(cancellation.movieId())
41+
.orElseThrow();
42+
43+
room.freeSeat(cancellation.seatNumber());
44+
screenRooms.save(room);
45+
}
46+
47+
@EventListener(ApplicationReadyEvent.class)
48+
void insertDummyMovies() {
49+
List<Movie> dummyMovies = LongStream.range(1, 30)
50+
.mapToObj(nr -> new Movie("Dummy movie #" + nr, "Screen #" + nr % 5, Instant.now()
51+
.plus(nr, HOURS)))
52+
.toList();
53+
screenRooms.saveAll(dummyMovies);
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.baeldung.spring.modulith.cqrs.movie;
2+
3+
import java.time.Instant;
4+
5+
import org.jmolecules.architecture.cqrs.QueryModel;
6+
7+
@QueryModel
8+
record UpcomingMovies(Long id, String title, Instant startTime) {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.baeldung.spring.modulith.cqrs.ticket;
2+
3+
import org.jmolecules.architecture.cqrs.Command;
4+
5+
@Command
6+
record BookTicket(Long movieId, String seat) {
7+
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.baeldung.spring.modulith.cqrs.ticket;
2+
3+
import java.time.Instant;
4+
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.GeneratedValue;
7+
import jakarta.persistence.Id;
8+
9+
@Entity
10+
class BookedTicket {
11+
12+
@Id
13+
@GeneratedValue
14+
private Long id;
15+
private Long movieId;
16+
private String seatNumber;
17+
private Instant createdAt = Instant.now();
18+
private Status status = Status.BOOKED;
19+
20+
BookedTicket cancelledBooking() {
21+
BookedTicket cancelled = new BookedTicket(movieId, seatNumber);
22+
cancelled.status = Status.BOOKING_CANCELLED;
23+
return cancelled;
24+
}
25+
26+
enum Status {
27+
BOOKED,
28+
BOOKING_CANCELLED
29+
}
30+
31+
boolean isBooked() {
32+
return status == Status.BOOKED;
33+
}
34+
35+
boolean isCancelled() {
36+
return status == Status.BOOKING_CANCELLED;
37+
}
38+
39+
BookedTicket(Long movieId, String seatNumber) {
40+
this.movieId = movieId;
41+
this.seatNumber = seatNumber;
42+
}
43+
44+
Long id() {
45+
return id;
46+
}
47+
48+
Long movieId() {
49+
return movieId;
50+
}
51+
52+
String seatNumber() {
53+
return seatNumber;
54+
}
55+
56+
Instant createdAt() {
57+
return createdAt;
58+
}
59+
60+
Status status() {
61+
return status;
62+
}
63+
64+
protected BookedTicket() {
65+
// Default constructor for JPA
66+
}
67+
}

0 commit comments

Comments
 (0)