Skip to content

Commit b328f88

Browse files
committed
🍮 Now Flight Tracker uses Command Query Segregation instead of Use Cases 🤭
1 parent e167e32 commit b328f88

File tree

23 files changed

+600
-223
lines changed

23 files changed

+600
-223
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.command;
2+
3+
/*
4+
* This interface is a marker interface for commands that return a result of type R.
5+
*/
6+
public interface Command<R> {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.command;
2+
3+
/*
4+
* This interface is a marker interface for command handlers that return a result of type R.
5+
*/
6+
public interface CommandHandler<C extends Command<R>, R> {
7+
8+
/*
9+
* This method handles a command and returns a result of type R.
10+
*/
11+
R handle(C command);
12+
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.mediator;
2+
3+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.command.Command;
4+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.query.Query;
5+
6+
/*
7+
* This interface is a mediator that sends commands and queries to the application context.
8+
*/
9+
public interface Mediator {
10+
11+
/*
12+
* This method sends a command to the application context and returns the result.
13+
*/
14+
<R> R send(Command<R> command);
15+
16+
/*
17+
* This method sends a query to the application context and returns the result.
18+
*/
19+
<R> R send(Query<R> query);
20+
21+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.mediator;
2+
3+
import org.springframework.context.ApplicationContext;
4+
import org.springframework.core.GenericTypeResolver;
5+
import org.springframework.stereotype.Component;
6+
7+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.command.Command;
8+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.command.CommandHandler;
9+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.query.Query;
10+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.query.QueryHandler;
11+
12+
import java.util.Arrays;
13+
import java.util.Objects;
14+
15+
/*
16+
* This class is a mediator that sends commands and queries to the application context.
17+
*/
18+
@Component
19+
public class SpringMediator implements Mediator {
20+
private final ApplicationContext applicationContext;
21+
22+
/*
23+
* This constructor injects the application context into the mediator.
24+
*/
25+
public SpringMediator(ApplicationContext applicationContext) {
26+
this.applicationContext = applicationContext;
27+
}
28+
29+
/*
30+
* This method sends a command to the application context and returns the result.
31+
*/
32+
@Override
33+
@SuppressWarnings("unchecked")
34+
public <R> R send(Command<R> command) {
35+
Objects.requireNonNull(command, "Command cannot be null");
36+
37+
Class<?> commandClass = command.getClass();
38+
39+
return Arrays.stream(applicationContext.getBeanNamesForType(CommandHandler.class))
40+
.map(applicationContext::getBean)
41+
.filter(handler -> {
42+
Class<?>[] generics = GenericTypeResolver.resolveTypeArguments(handler.getClass(), CommandHandler.class);
43+
return generics != null && generics[0].equals(commandClass);
44+
})
45+
.findFirst()
46+
.map(
47+
handler -> ((CommandHandler<Command<R>, R>) handler).handle(command)
48+
)
49+
.orElseThrow(
50+
() -> new IllegalStateException("No handler found for command: %s".formatted(commandClass.getName()))
51+
);
52+
}
53+
54+
/*
55+
* This method sends a query to the application context and returns the result.
56+
*/
57+
@Override
58+
@SuppressWarnings("unchecked")
59+
public <R> R send(Query<R> query) {
60+
Objects.requireNonNull(query, "Query cannot be null");
61+
62+
Class<?> queryClass = query.getClass();
63+
64+
return Arrays.stream(applicationContext.getBeanNamesForType(QueryHandler.class))
65+
.map(applicationContext::getBean)
66+
.filter(handler -> {
67+
Class<?>[] generics = GenericTypeResolver.resolveTypeArguments(handler.getClass(), QueryHandler.class);
68+
return generics != null && generics[0].equals(queryClass);
69+
})
70+
.findFirst()
71+
.map(
72+
handler -> (
73+
(QueryHandler<Query<R>, R>) handler).handle(query)
74+
)
75+
.orElseThrow(
76+
() -> new IllegalStateException("No handler found for query: %s".formatted(queryClass.getName()))
77+
);
78+
}
79+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.query;
2+
3+
/*
4+
* This interface is a marker interface for queries that return a result of type R.
5+
*/
6+
public interface Query<R> {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.luismachadoreis.flighttracker.server.common.application.cqs.query;
2+
3+
/*
4+
* This interface is a marker interface for query handlers that return a result of type R.
5+
*/
6+
public interface QueryHandler<Q extends Query<R>, R> {
7+
8+
/*
9+
* This method handles a query and returns a result of type R.
10+
*/
11+
R handle(Q query);
12+
13+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package dev.luismachadoreis.flighttracker.server.ping.infrastructure.kafka;
1+
package dev.luismachadoreis.flighttracker.server.flightdata.infrastructure.kafka;
22

33
import dev.luismachadoreis.flighttracker.server.ping.application.dto.FlightDataDTO;
44

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
package dev.luismachadoreis.flighttracker.server.ping.infrastructure.pubsub.flightdata;
1+
package dev.luismachadoreis.flighttracker.server.flightdata.infrastructure.pubsub;
22

3+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.mediator.Mediator;
4+
import dev.luismachadoreis.flighttracker.server.ping.application.CreatePingCommand;
35
import dev.luismachadoreis.flighttracker.server.ping.application.dto.FlightDataDTO;
46
import dev.luismachadoreis.flighttracker.server.ping.application.dto.PingDTOMapper;
5-
import dev.luismachadoreis.flighttracker.server.ping.application.usecase.CreatePingUseCase;
67
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
89
import org.springframework.kafka.annotation.KafkaListener;
@@ -13,12 +14,12 @@
1314
@RequiredArgsConstructor
1415
public class FlightDataSubscriber {
1516

16-
private final CreatePingUseCase createPingUseCase;
17+
private final Mediator mediator;
1718

1819
@KafkaListener(topics = "${spring.kafka.topic.flight-positions}", groupId = "${spring.kafka.consumer.group-id}")
1920
public void consumeFlightData(FlightDataDTO data) {
2021
log.debug("Received flight data: {}", data);
21-
createPingUseCase.execute(PingDTOMapper.fromFlightData(data));
22+
mediator.send(new CreatePingCommand(PingDTOMapper.fromFlightData(data)));
2223
}
2324

2425
}
Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
package dev.luismachadoreis.flighttracker.server.ping.api;
22

3+
import dev.luismachadoreis.flighttracker.server.ping.application.CreatePingCommand;
4+
import dev.luismachadoreis.flighttracker.server.ping.application.GetRecentPingsQuery;
35
import dev.luismachadoreis.flighttracker.server.ping.application.dto.PingDTO;
4-
import dev.luismachadoreis.flighttracker.server.ping.application.usecase.CreatePingUseCase;
5-
import dev.luismachadoreis.flighttracker.server.ping.application.usecase.GetPingsUseCase;
6-
import io.swagger.v3.oas.annotations.Operation;
7-
import io.swagger.v3.oas.annotations.Parameter;
8-
import io.swagger.v3.oas.annotations.media.Content;
9-
import io.swagger.v3.oas.annotations.media.Schema;
10-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
11-
import io.swagger.v3.oas.annotations.tags.Tag;
12-
import org.springframework.beans.factory.annotation.Value;
6+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.mediator.Mediator;
7+
138
import org.springframework.http.ResponseEntity;
149
import org.springframework.web.bind.annotation.*;
10+
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
1511

12+
import java.net.URI;
1613
import java.util.List;
14+
import java.util.UUID;
1715
import java.util.Optional;
16+
import lombok.extern.slf4j.Slf4j;
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import io.swagger.v3.oas.annotations.Operation;
19+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
20+
import io.swagger.v3.oas.annotations.media.Content;
21+
import io.swagger.v3.oas.annotations.media.Schema;
22+
import org.springframework.beans.factory.annotation.Value;
23+
import org.springframework.web.bind.annotation.RequestMapping;
24+
import org.springframework.web.bind.annotation.RestController;
1825

19-
@Tag(name = "Ping API", description = "API for managing flight position pings")
26+
@Slf4j
2027
@RestController
21-
@RequestMapping("/api/pings")
28+
@RequestMapping("/api/v2/pings")
29+
@Tag(name = "Ping API", description = "API for managing flight position pings")
2230
public class PingController {
2331

24-
private final CreatePingUseCase createPingUseCase;
25-
private final GetPingsUseCase getPingsUseCase;
26-
private final int defaultRequestLimit;
32+
private final Mediator mediator;
33+
private final Integer defaultRequestLimit;
2734

28-
public PingController(CreatePingUseCase createPingUseCase, GetPingsUseCase getPingsUseCase, @Value("${default.request.limit:50}") int defaultRequestLimit) {
29-
this.createPingUseCase = createPingUseCase;
30-
this.getPingsUseCase = getPingsUseCase;
35+
public PingController(Mediator mediator, @Value("${default.request.limit:50}") Integer defaultRequestLimit) {
36+
this.mediator = mediator;
3137
this.defaultRequestLimit = defaultRequestLimit;
3238
}
3339

@@ -43,8 +49,17 @@ public PingController(CreatePingUseCase createPingUseCase, GetPingsUseCase getPi
4349
}
4450
)
4551
@PostMapping
46-
public ResponseEntity<PingDTO> createPing(@RequestBody PingDTO pingDTO) {
47-
return ResponseEntity.ok(createPingUseCase.execute(pingDTO));
52+
public ResponseEntity<Void> createPing(@RequestBody PingDTO pingDTO) {
53+
UUID pingId = mediator.send(new CreatePingCommand(pingDTO));
54+
55+
URI location = ServletUriComponentsBuilder
56+
.fromCurrentRequest()
57+
.path("/{id}")
58+
.buildAndExpand(pingId)
59+
.toUri();
60+
61+
log.info("Ping created with id: {}", pingId);
62+
return ResponseEntity.created(location).build();
4863
}
4964

5065
@Operation(
@@ -59,15 +74,15 @@ public ResponseEntity<PingDTO> createPing(@RequestBody PingDTO pingDTO) {
5974
}
6075
)
6176
@GetMapping
62-
public ResponseEntity<List<PingDTO>> getPings(
63-
@Parameter(
64-
description = "Maximum number of pings to retrieve",
65-
example = "50"
66-
)
67-
@RequestParam(required = false) Integer limit
68-
) {
69-
return ResponseEntity.ok(getPingsUseCase.execute(
70-
Optional.ofNullable(limit).orElse(defaultRequestLimit)
71-
));
77+
public ResponseEntity<List<PingDTO>> getRecentPings(@RequestParam(required = false) Integer limit) {
78+
List<PingDTO> pings = mediator.send(
79+
new GetRecentPingsQuery(
80+
Optional.ofNullable(limit).orElse(defaultRequestLimit)
81+
)
82+
);
83+
84+
log.info("Recent pings retrieved: {}", pings);
85+
return ResponseEntity.ok(pings);
7286
}
87+
7388
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.luismachadoreis.flighttracker.server.ping.application;
2+
3+
import dev.luismachadoreis.flighttracker.server.common.application.cqs.command.Command;
4+
import dev.luismachadoreis.flighttracker.server.ping.application.dto.PingDTO;
5+
6+
import java.util.UUID;
7+
8+
/*
9+
* This record is a command that creates a ping.
10+
*/
11+
public record CreatePingCommand(PingDTO pingDTO) implements Command<UUID> {}

0 commit comments

Comments
 (0)