├── .gitignore
├── README.md
├── diagrams
└── library.xml
├── pom.xml
└── src
└── main
└── java
└── org
└── sgitario
└── axon
└── library
├── Application.java
├── LibraryRestController.java
├── aggregate
└── Library.java
├── commands
├── RegisterBookCommand.java
└── RegisterLibraryCommand.java
├── events
├── BookCreatedEvent.java
└── LibraryCreatedEvent.java
├── models
├── BookBean.java
└── LibraryBean.java
├── queries
├── GetBooksQuery.java
└── GetLibraryQuery.java
└── repository
├── BookEntity.java
├── BookRepository.java
├── BookRepositoryProjector.java
└── LibraryProjector.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .classpath
3 | .project
4 | .settings
5 | target
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Axon - CQRS with Spring Boot by examples
4 | date: 2018-10-23
5 | tags: [ Axon, Java, Architecture ]
6 | ---
7 |
8 | We already introduced [CQRS](https://martinfowler.com/bliki/CQRS.html) architectures a while ago [here](https://sgitario.github.io/applying-event-sourcing-and-qcrs-in/). The focus was to migrate a legacy monolith application into a CQRS architecture. However, we didn't go in deep by actually coding it. Let's do this using [Axon Framework](https://axoniq.io/) and Spring Boot. We'll create a library application from scratch.
9 |
10 | # Some Concepts First
11 |
12 | [Axon](https://axoniq.io/) is not just a framework, but an infrastructure that also involves an Axon server. The Axon server manages a bus event and all the mecanisms to manage the commands and queries:
13 |
14 | 
15 |
16 | This image is taken from [the official Axon documentation](https://docs.axoniq.io/reference-guide/architecture-overview) for version 4.
17 |
18 | ## Event Store
19 |
20 | Where all our events will be stored? In the Event Store running under the Axon Server (see *AxonServerEventStore.java* for more information). If we want to use an embedded event store in, let's say, a RDBMS instance or in Mongo, Axon provides custom implementations for this. More in [here](https://docs.axoniq.io/reference-guide/1.3-infrastructure-components/repository-and-event-store#jdbceventstorageengine). This is something we'll like to explore further in the future.
21 |
22 | ## Aggregates
23 |
24 | Aggregates are the domain objects in Axon. We can configure our configuration to match the aggregate to an entity in our database.
25 |
26 | When we connect our application, Events will be read on-demand when an existing aggregate is targeted by a command. To enhance efficiency when processing multiple commands to the same aggregate, Axon supports caching repositories that would keep the aggregate in memory to avoid another read of the same events. Once aggregates are totally synchronized, it will be ready to be used. See [*AggregateLifecycle.java*](https://axoniq.io/apidocs/3.1/org/axonframework/commandhandling/model/AggregateLifecycle.html).
27 |
28 | # Getting Started
29 |
30 | ## Run Axon Server
31 |
32 | We'll use [the official docker image](https://hub.docker.com/r/axoniq/axonserver/) to startup an Axon server instance:
33 |
34 | ```
35 | docker run -d --name axonserver -p 8024:8024 -p 8124:8124 axoniq/axonserver
36 | ```
37 |
38 | Feel free to startup a server instance yourself by downloading the binaries from [here](http://www.axonframework.org/download).
39 |
40 | In order to check the installation was succeded, browse to "http://localhost:8024/" and we should see the Axon dashboard:
41 |
42 | 
43 |
44 | ## Axon Framework: Maven Dependencies
45 |
46 | We'll use [the Axon Spring Boot Starter maven dependency](https://mvnrepository.com/artifact/org.axonframework/axon-core/4.0-M2) in the *pom.xml* for all our projects:
47 |
48 | ```xml
49 |
50 | org.axonframework
51 | axon-spring-boot-starter
52 | 4.0
53 |
54 |
55 |
56 | org.springframework.boot
57 | spring-boot-starter-web
58 | 2.0.6.RELEASE
59 |
60 | ```
61 |
62 | This is the easiest way to get warm with Axon. Spring Boot eases the configuration using default components in Axon. For more information, go to [here](https://docs.axoniq.io/reference-guide/1.3-infrastructure-components/spring-boot-autoconfiguration).
63 |
64 | Also, Axon provides a very good tutorial or [recipe with Spring Boot](https://docs.axoniq.io/axon-cookbook/basic-recipes/simple-application-using-axon-framework-and-spring-boot).
65 |
66 | # The Library Application
67 |
68 | Let's start coding! We'll write a library application where we can organize books into different libraries.
69 |
70 | 
71 |
72 | We need to start thinking on events. What is a library? In object oriented programming, a library is just a set of books. In event oriented programming, a library is:
73 |
74 | - event 1: library named "My Library"
75 | - event 2: Added book "x" to "My Library"
76 | - event 3: Added book "y" to "My Library"
77 |
78 | Note, we can redefine the concept of libraries anytime by adding a new event. And the most important, we have a time-scale of our library thanks to events.
79 |
80 | On the other hand, we have the commands. A command is an use case in our application and it can derivate in a set of events that will define the current state.
81 |
82 | Therefore, let's continue coding our application.
83 |
84 | ## Application: Spring Boot
85 |
86 | We'll use Spring Boot which is the easiest way to startup our application:
87 |
88 | ```java
89 | @SpringBootApplication
90 | public class Application {
91 |
92 | public static void main(String[] args) {
93 | SpringApplication.run(Application.class, args);
94 | }
95 | }
96 | ```
97 |
98 | **This is only for testing purposes.** For production, we'd need to provide a persistence state of the event store and use a distributed queue. We'll write about these refinements in a future post.
99 |
100 | ## Commands: Use Cases
101 |
102 | The commands are the use cases of our application and needs to verify the data is correct before adding an event. Therefore, we don't need to validate the events afterwards.
103 |
104 | **- Register a Library**
105 |
106 | Let's define the command to register a new library:
107 |
108 | ```java
109 | public class RegisterLibraryCommand {
110 | @TargetAggregateIdentifier
111 | private final Integer libraryId;
112 |
113 | private final String name;
114 |
115 | // Constructor and getters
116 | }
117 | ```
118 |
119 | We'll identify our libraries by an integer and a name.
120 |
121 | Let's start writing the Library aggregate:
122 |
123 | ```java
124 | @Aggregate
125 | public class Library {
126 |
127 | @AggregateIdentifier
128 | private Integer libraryId;
129 | private String name;
130 | private List isbnBooks;
131 |
132 | protected Library() {
133 | // For Axon instantiation
134 | }
135 |
136 | @CommandHandler
137 | public Library(RegisterLibraryCommand cmd) {
138 | Assert.notNull(cmd.getLibraryId(), "ID should not be null");
139 | Assert.notNull(cmd.getName(), "Name should not be null");
140 |
141 | AggregateLifecycle.apply(new LibraryCreatedEvent(cmd.getLibraryId(), cmd.getName()));
142 | }
143 |
144 | // getters
145 | }
146 | ```
147 |
148 | Axon Spring Boot starter will scan for classes annotated with *@Aggregate* and register it into the Axon application. The same for methods annotated with *@CommandHandler* where the Axon framework will invoke them after trigger a command of the matching type.
149 |
150 | The constructor with the *RegisterLibraryCommand* command states the aggregate will be created using this command. This can be also achieved via:
151 |
152 | ```java
153 | AggregateLifecycle.createNew(Library.class, Library::new);
154 | ```
155 |
156 | The other protected constructor is used by Axon to instantiate existing aggregates from events.
157 |
158 | And as we already said, the events define the state of the aggregates, so we need to register this creation with a new event:
159 |
160 | ```java
161 | public class BookCreatedEvent {
162 | @TargetAggregateIdentifier
163 | private final Integer libraryId;
164 | private final String isbn;
165 | private final String title;
166 |
167 | // constructor and getters
168 | }
169 | ```
170 |
171 | Hold on... Something is missing... where are we setting the library data? Not in the command handler: we need to validate the data in the commands, but we should not set the data here. Why? As we already said, after our application started and joined to the Axon server, it will synchronize the events in the server and create the required aggregates. Therefore, we need to set the data when handling the events:
172 |
173 | ```java
174 | @Aggregate
175 | public class Library {
176 |
177 | @AggregateIdentifier
178 | private Integer libraryId;
179 | private String name;
180 | private List isbnBooks;
181 |
182 | protected Library() {
183 | // For Axon instantiation
184 | }
185 |
186 | @CommandHandler
187 | public Library(RegisterLibraryCommand cmd) {
188 | Assert.notNull(cmd.getLibraryId(), "ID should not be null");
189 | Assert.notNull(cmd.getName(), "Name should not be null");
190 |
191 | AggregateLifecycle.apply(new LibraryCreatedEvent(cmd.getLibraryId(), cmd.getName()));
192 | }
193 |
194 | // getters
195 |
196 | @EventSourcingHandler
197 | private void handleCreatedEvent(LibraryCreatedEvent event) {
198 | libraryId = event.getLibraryId();
199 | name = event.getName();
200 | isbnBooks = new ArrayList<>();
201 | }
202 |
203 | }
204 | ```
205 |
206 | We annotate the methods with *@EventSourcingHandler* to listen events of a concrete type and that manipulate the aggregates. **Don't confuse with the annotation *@EventHandler* which is used outside of an aggregate class. We'll see an example later.**
207 |
208 | **- Register a Book**
209 |
210 | "I want to add a new book to my library". Let's write this command:
211 |
212 | ```java
213 | public class RegisterBookCommand {
214 | @TargetAggregateIdentifier
215 | private final Integer libraryId;
216 | private final String isbn;
217 | private final String title;
218 |
219 | // constructor and getters
220 | }
221 | ```
222 |
223 | And let's update our aggregate:
224 |
225 | ```java
226 | @Aggregate
227 | public class Library {
228 |
229 | @AggregateIdentifier
230 | private Integer libraryId;
231 | private String name;
232 | private List isbnBooks;
233 |
234 | // constructors
235 |
236 | // getters
237 |
238 | // handleCreatedEvent command
239 |
240 | @CommandHandler
241 | public void addBook(RegisterBookCommand cmd) {
242 | Assert.notNull(cmd.getLibraryId(), "ID should not be null");
243 | Assert.notNull(cmd.getIsbn(), "Book ISBN should not be null");
244 |
245 | AggregateLifecycle.apply(new BookCreatedEvent(cmd.getLibraryId(), cmd.getIsbn(), cmd.getTitle()));
246 | }
247 |
248 | @EventSourcingHandler
249 | private void addBook(BookCreatedEvent event) {
250 | isbnBooks.add(event.getIsbn());
251 | }
252 |
253 | }
254 | ```
255 |
256 | Where *BookCreatedEvent* class is:
257 |
258 | ```java
259 | public class BookCreatedEvent {
260 | @TargetAggregateIdentifier
261 | private final Integer libraryId;
262 | private final String isbn;
263 | private final String title;
264 |
265 | // constructor and getters
266 | }
267 | ```
268 |
269 | Nothing new so far, but wait a second: we're only using the ISBN book identifier in the aggregate, so we're losing the title! Kind of. This is because we wanted to use another repository to store the book data by listening to the *BookCreatedEvent* event outside of the aggregate.
270 |
271 | We'll use a JPA repository and H2 as an in-memory database. The only thing we need to add is to add these two maven dependencies:
272 |
273 | ```xml
274 |
275 | org.springframework.boot
276 | spring-boot-starter-data-jpa
277 | 2.0.6.RELEASE
278 |
279 |
280 |
281 | com.h2database
282 | h2
283 | 1.4.197
284 |
285 | ```
286 |
287 | Then, create the repository using Spring Data:
288 |
289 | ```java
290 | @Repository
291 | public interface BookRepository extends CrudRepository {
292 | List findByLibraryId(Integer libraryId);
293 | }
294 | ```
295 |
296 | And finally, add a service to listen events using the *@EventHandler* annotation:
297 |
298 | ```java
299 | @Service
300 | public class BookRepositoryProjector {
301 |
302 | private final BookRepository bookRepository;
303 |
304 | public BookRepositoryProjector(BookRepository bookRepository) {
305 | this.bookRepository = bookRepository;
306 | }
307 |
308 | @EventHandler
309 | public void addBook(BookCreatedEvent event) throws Exception {
310 | BookEntity book = new BookEntity();
311 | book.setIsbn(event.getIsbn());
312 | book.setLibraryId(event.getLibraryId());
313 | book.setTitle(event.getTitle());
314 | bookRepository.save(book);
315 | }
316 | }
317 | ```
318 |
319 | And that's all!
320 |
321 | ## Queries
322 |
323 | We are done with the use cases and commands. We'll work now about how to expose our data outside using the *@QueryHandler* annotation.
324 |
325 | **Important: *@QueryHandler* annotations do not work on aggregate classes.**
326 |
327 | **- Query List of Books**
328 |
329 | If we own the repository where our data is stored, we could only read the data from the repository provided by Spring Data:
330 |
331 | ```java
332 | @Service
333 | public class BookRepositoryProjector {
334 |
335 | private final BookRepository bookRepository;
336 |
337 | // constructor
338 |
339 | // add book method
340 |
341 | @QueryHandler
342 | public List getBooks(GetBooksQuery query) {
343 | return bookRepository.findByLibraryId(query.getLibraryId()).stream()
344 | .map(e -> {
345 | BookBean book = new BookBean();
346 | book.setIsbn(e.getIsbn());
347 | book.setTitle(e.getTitle());
348 | return book;
349 | }).collect(Collectors.toList());
350 | }
351 | }
352 | ```
353 |
354 | Where the *GetBooksQuery* class is:
355 |
356 | ```java
357 | public class GetBooksQuery {
358 | private final Integer libraryId;
359 |
360 | // constructor and getter
361 | }
362 | ```
363 |
364 | **- Get Library aggregate**
365 |
366 | However, most of the times we will want the current state of an aggregate since this would be the view of our business. We need to instantiate the *Repository* of the Event Store: where the aggregates are. Thanks to Spring, the correct implementation is already in the application context and all we need is:
367 |
368 | ```java
369 | @Service
370 | public class LibraryProjector {
371 | private final Repository libraryRepository;
372 |
373 | public LibraryProjector(Repository libraryRepository) {
374 | this.libraryRepository = libraryRepository;
375 | }
376 |
377 | @QueryHandler
378 | public Library getLibrary(GetLibraryQuery query) throws InterruptedException, ExecutionException {
379 | CompletableFuture future = new CompletableFuture();
380 | libraryRepository.load("" + query.getLibraryId()).execute(future::complete);
381 | return future.get();
382 | }
383 |
384 | }
385 | ```
386 |
387 | Where the *GetLibraryQuery* class is:
388 |
389 | ```java
390 | public class GetLibraryQuery {
391 | private final Integer libraryId;
392 |
393 | // constructor and getter
394 | }
395 | ```
396 |
397 | Something odd is that the *load* method only accepts String for the aggregate identifiers and we use an integer. That's why we needed to cast it to String.
398 |
399 | ## REST layer
400 |
401 | Right, we now know how to deal with command and queries, but how can we invoke or send these commands or events?
402 |
403 | Axon provides a *CommandGateway* interface to trigger commands:
404 |
405 | ```java
406 | commandGateway.send(new MyCommand(...));
407 | ```
408 |
409 | And it also provides a *QueryGateway* interface to trigger queries:
410 |
411 | ```java
412 | CompletableFuture future = queryGateway.query(new MyQuery(...), Library.class);
413 | return future.get();
414 | ```
415 |
416 | Therefore, this is how would look like our REST API:
417 |
418 | ```java
419 | @RestController
420 | public class LibraryRestController {
421 |
422 | private final CommandGateway commandGateway;
423 | private final QueryGateway queryGateway;
424 |
425 | @Autowired
426 | public LibraryRestController(CommandGateway commandGateway, QueryGateway queryGateway) {
427 | this.commandGateway = commandGateway;
428 | this.queryGateway = queryGateway;
429 | }
430 |
431 | @PostMapping("/api/library")
432 | public String addLibrary(@RequestBody LibraryBean library) {
433 | commandGateway.send(new RegisterLibraryCommand(library.getLibraryId(), library.getName()));
434 | return "Saved";
435 | }
436 |
437 | @GetMapping("/api/library/{library}")
438 | public Library getLibrary(@PathVariable Integer library) throws InterruptedException, ExecutionException {
439 | return queryGateway.query(new GetLibraryQuery(library), Library.class).get();
440 | }
441 |
442 | @PostMapping("/api/library/{library}/book")
443 | public String addBook(@PathVariable Integer library, @RequestBody BookBean book) {
444 | commandGateway.send(new RegisterBookCommand(library, book.getIsbn(), book.getTitle()),
445 | LoggingCallback.INSTANCE);
446 | return "Added";
447 | }
448 |
449 | @GetMapping("/api/library/{library}/book")
450 | public List addBook(@PathVariable Integer library) throws InterruptedException, ExecutionException {
451 | return queryGateway.query(new GetBooksQuery(library), ResponseTypes.multipleInstancesOf(BookBean.class)).get();
452 | }
453 |
454 | }
455 | ```
456 |
457 | ## Run
458 |
459 | We can run our application as an usual Spring Boot application. If you see the following in the output:
460 |
461 | ```
462 | **********************************************
463 | * *
464 | * !!! UNABLE TO CONNECT TO AXON SERVER !!! *
465 | * *
466 | * Are you sure it's running? *
467 | * If you haven't got Axon Server yet, visit *
468 | * https://axoniq.io/download *
469 | * *
470 | **********************************************
471 | ```
472 |
473 | This message means we cannot connect with the Axon server. Double check the Axon server is up and running and/or go to the Axon dashboard in the browser.
474 |
475 | - Add a library:
476 |
477 | ```
478 | POST: http://localhost:8080/api/library
479 | BODY:
480 | {
481 | "libraryId": 1,
482 | "name": "My Library"
483 | }
484 | ```
485 |
486 | - Add a book:
487 |
488 | ```
489 | POST: http://localhost:8080/api/library/1/book
490 | BODY:
491 | {
492 | "isbn":"123460",
493 | "title": "My Title",
494 | "author": "Jose Carvajal"
495 | }
496 | ```
497 |
498 | - Get books in a library:
499 |
500 | ```
501 | GET: http://localhost:8080/api/library/1/book
502 | ```
503 |
504 | - Get Library:
505 |
506 | ```
507 | GET: http://localhost:8080/api/library/1
508 | ```
509 |
510 | # Conclusion
511 |
512 | We have introduced some of the most important features in Axon and how Axon works. Axon Framework makes extremely easy to implement a CQRS architecture and is very well integrated to Spring and Spring Cloud (for doing the services discoverable). Also, Axon is **VERY** customisable. We can refine until the most tiny component in your architecture which demostrates what a well designed the framework is.
513 |
514 | On the other side, even when the documentation is quite extensive and good, I would have appreciated working examples for version 4 of basic functionality. It does not help either when the Axon documentation redirects to the first chapter when you search something in google.
515 |
516 | See [my Github repository](https://github.com/Sgitario/axon-getting-started) for a full example.
--------------------------------------------------------------------------------
/diagrams/library.xml:
--------------------------------------------------------------------------------
1 | 7Vldc6M2FP01fuwOIMDkMXa824d0uq07/XgUSAY1AnmEHNv99ZWCBAKRmEmxs+00L0EH0Mc591x05QVYl6cvHO6LHxjCdBF46LQAD4sgCIDvyX8KOTeI74GgQXJOkMY6YEv+wuZBjR4IwnXvQcEYFWTfBzNWVTgTPQxyzo79x3aM9kfdwxw7wDaD1EV/I0gUDZoEyw7/HpO8MCP78V1zJ4XZU87ZodLjLQKwe/lrbpfQ9KUXWhcQsaMFgc0CrDljorkqT2tMFbmGtua9z6/cbefNcSUmvRA1bzxDesBmyi8TE2dDxrEgAm/3MFPto1R8AVaFKKls+fJyRyhdM8q4bFeswgpilfgMS0KV/r9ijmAFNay19uVKV+5k9fyfMRf4ZEF68l8wK7HgZ/mIvhsuNZE60oJQt4+dbMGdxoqeZBqEOlTytu+OLnmhGRtn7+4yebhC9yogZSujsK5J1mcPn4j4XV57nyLd+sPwOk7iq6TV7MAzPWqoPQN5jvVToIEw6kW+S6xFXDTCm8E4plCQ575fxrjUI3xlRM63083r6+ZHAzma1ei37AAedASifkcgHnTUcOB09CJtu+xJai9vaJUZrOEHl60BPHAla4QOWfcISWDF2NMiiKkca5VyeZWLdrkWkS8pFCPN2yVaX0k3FtsqDycZzpT7asHZE7bupEkUyjCah/VwwHocOay3ytisD+P2PaS7ybwh/ZGkHOqu/hMsB8kHspw4LK9ZWcIK1Q7Bcjmiz2KfFpME3LwAKckr9dGQJGGJrxQ5RO5Q7vWNkiCkhhmVrRPWM99dPam3lLQ+z2AmN/iXc1D7jK1TMINOvpuDWiN4Px2w+j9zIup7AUGc7LJphEdjlomzBKe7eaSIwQdaxo8dKZQABH/Llplgkb6zZtjODvPaiF/Ca9nF3c3+vNn+Mq8jIpygcHqF4DgiCVIQx/M4AtwN9qBgJDldyxHtdP9B7TAxrXQlRjCsMV5l0S4mTGDY1YTeC38j1QQYVBPR0A/vrSba8nL+asLofYsAqOSsmgjwl0sDqBj4zvvkBS3yVWZkuQKVOx/e3Khdig5zqvB/eLw7PEDoRoNk03xsGBcFy1kF6aZDB9+wsVMGy//6xKGLjciODPXWn1iIsw4keBBMQt24j4zte1lkIPbFwDEnEk5accNksv5Tsy+IrsGu/ya7sm6w+X3LeLcnvnHsDZg3g1tpb/MsZys3gt5WMI4dYeoC7tVldqZE8s8vbzrSRqjHtAXaU9kfD0L20hZCg9oVoiWcuF8f3Z2kYRx54Uwnm4Pjm/bEzMqZ8UjOTGbYnLgaPaz8mwoz7azB6HVVIfzlBwrhKqEO0Op/aQHrkD4izfTa6IoFrGx2v8E0X+/uly6w+Rs=
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
4 | 4.0.0
5 | org.sgitario.axon
6 | bookstore
7 | 0.0.1-SNAPSHOT
8 |
9 |
10 | 1.8
11 | 1.8
12 | 2.0.6.RELEASE
13 |
14 |
15 |
16 |
17 | org.springframework.boot
18 | spring-boot-starter-web
19 | ${spring.boot.version}
20 |
21 |
22 |
23 | org.axonframework
24 | axon-spring-boot-starter
25 | 4.0
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-data-jpa
31 | ${spring.boot.version}
32 |
33 |
34 |
35 | com.h2database
36 | h2
37 | 1.4.197
38 |
39 |
40 |
41 | org.projectlombok
42 | lombok
43 | 1.18.2
44 | provided
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/Application.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class Application {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(Application.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/LibraryRestController.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library;
2 |
3 | import java.util.List;
4 | import java.util.concurrent.CompletableFuture;
5 | import java.util.concurrent.ExecutionException;
6 |
7 | import org.axonframework.commandhandling.gateway.CommandGateway;
8 | import org.axonframework.messaging.responsetypes.ResponseTypes;
9 | import org.axonframework.queryhandling.QueryGateway;
10 | import org.sgitario.axon.library.aggregate.Library;
11 | import org.sgitario.axon.library.commands.RegisterBookCommand;
12 | import org.sgitario.axon.library.commands.RegisterLibraryCommand;
13 | import org.sgitario.axon.library.models.BookBean;
14 | import org.sgitario.axon.library.models.LibraryBean;
15 | import org.sgitario.axon.library.queries.GetBooksQuery;
16 | import org.sgitario.axon.library.queries.GetLibraryQuery;
17 | import org.springframework.beans.factory.annotation.Autowired;
18 | import org.springframework.web.bind.annotation.GetMapping;
19 | import org.springframework.web.bind.annotation.PathVariable;
20 | import org.springframework.web.bind.annotation.PostMapping;
21 | import org.springframework.web.bind.annotation.RequestBody;
22 | import org.springframework.web.bind.annotation.RestController;
23 |
24 | @RestController
25 | public class LibraryRestController {
26 |
27 | private final CommandGateway commandGateway;
28 | private final QueryGateway queryGateway;
29 |
30 | @Autowired
31 | public LibraryRestController(CommandGateway commandGateway, QueryGateway queryGateway) {
32 | this.commandGateway = commandGateway;
33 | this.queryGateway = queryGateway;
34 | }
35 |
36 | @PostMapping("/api/library")
37 | public String addLibrary(@RequestBody LibraryBean library) {
38 | commandGateway.send(new RegisterLibraryCommand(library.getLibraryId(), library.getName()));
39 | return "Saved";
40 | }
41 |
42 | @GetMapping("/api/library/{library}")
43 | public Library getLibrary(@PathVariable Integer library) throws InterruptedException, ExecutionException {
44 | CompletableFuture future = queryGateway.query(new GetLibraryQuery(library), Library.class);
45 | return future.get();
46 | }
47 |
48 | @PostMapping("/api/library/{library}/book")
49 | public String addBook(@PathVariable Integer library, @RequestBody BookBean book) {
50 | commandGateway.send(new RegisterBookCommand(library, book.getIsbn(), book.getTitle()));
51 | return "Saved";
52 | }
53 |
54 | @GetMapping("/api/library/{library}/book")
55 | public List addBook(@PathVariable Integer library) throws InterruptedException, ExecutionException {
56 | return queryGateway.query(new GetBooksQuery(library), ResponseTypes.multipleInstancesOf(BookBean.class)).get();
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/aggregate/Library.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.aggregate;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import org.axonframework.commandhandling.CommandHandler;
7 | import org.axonframework.eventsourcing.EventSourcingHandler;
8 | import org.axonframework.modelling.command.AggregateIdentifier;
9 | import org.axonframework.modelling.command.AggregateLifecycle;
10 | import org.axonframework.spring.stereotype.Aggregate;
11 | import org.sgitario.axon.library.commands.RegisterBookCommand;
12 | import org.sgitario.axon.library.commands.RegisterLibraryCommand;
13 | import org.sgitario.axon.library.events.BookCreatedEvent;
14 | import org.sgitario.axon.library.events.LibraryCreatedEvent;
15 | import org.springframework.util.Assert;
16 |
17 | @Aggregate
18 | public class Library {
19 |
20 | @AggregateIdentifier
21 | private Integer libraryId;
22 |
23 | private String name;
24 |
25 | private List isbnBooks;
26 |
27 | protected Library() {
28 | // For Axon instantiation
29 | }
30 |
31 | @CommandHandler
32 | public Library(RegisterLibraryCommand cmd) {
33 | Assert.notNull(cmd.getLibraryId(), "ID should not be null");
34 | Assert.notNull(cmd.getName(), "Name should not be null");
35 |
36 | AggregateLifecycle.apply(new LibraryCreatedEvent(cmd.getLibraryId(), cmd.getName()));
37 | }
38 |
39 | public Integer getLibraryId() {
40 | return libraryId;
41 | }
42 |
43 | public String getName() {
44 | return name;
45 | }
46 |
47 | public List getIsbnBooks() {
48 | return isbnBooks;
49 | }
50 |
51 | @CommandHandler
52 | public void addBook(RegisterBookCommand cmd) {
53 | Assert.notNull(cmd.getLibraryId(), "ID should not be null");
54 | Assert.notNull(cmd.getIsbn(), "Book ISBN should not be null");
55 |
56 | AggregateLifecycle.apply(new BookCreatedEvent(cmd.getLibraryId(), cmd.getIsbn(), cmd.getTitle()));
57 | }
58 |
59 | @EventSourcingHandler
60 | private void handleCreatedEvent(LibraryCreatedEvent event) {
61 | libraryId = event.getLibraryId();
62 | name = event.getName();
63 | isbnBooks = new ArrayList<>();
64 | }
65 |
66 | @EventSourcingHandler
67 | private void addBook(BookCreatedEvent event) {
68 | isbnBooks.add(event.getIsbn());
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/commands/RegisterBookCommand.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.commands;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | import lombok.Data;
6 |
7 | @Data
8 | public class RegisterBookCommand {
9 | @TargetAggregateIdentifier
10 | private final Integer libraryId;
11 | private final String isbn;
12 | private final String title;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/commands/RegisterLibraryCommand.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.commands;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | import lombok.Data;
6 |
7 | @Data
8 | public class RegisterLibraryCommand {
9 | @TargetAggregateIdentifier
10 | private final Integer libraryId;
11 |
12 | private final String name;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/events/BookCreatedEvent.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.events;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | import lombok.Data;
6 |
7 | @Data
8 | public class BookCreatedEvent {
9 | @TargetAggregateIdentifier
10 | private final Integer libraryId;
11 | private final String isbn;
12 | private final String title;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/events/LibraryCreatedEvent.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.events;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class LibraryCreatedEvent {
7 | private final Integer libraryId;
8 | private final String name;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/models/BookBean.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.models;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class BookBean {
7 | private String isbn;
8 | private String title;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/models/LibraryBean.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.models;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class LibraryBean {
7 | private int libraryId;
8 | private String name;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/queries/GetBooksQuery.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.queries;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class GetBooksQuery {
7 | private final Integer libraryId;
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/queries/GetLibraryQuery.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.queries;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class GetLibraryQuery {
7 | private final Integer libraryId;
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/repository/BookEntity.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.repository;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Entity;
5 | import javax.persistence.Id;
6 |
7 | import lombok.Data;
8 |
9 | @Entity
10 | @Data
11 | public class BookEntity {
12 | @Id
13 | private String isbn;
14 | @Column
15 | private int libraryId;
16 | @Column
17 | private String title;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/repository/BookRepository.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.repository;
2 |
3 | import java.util.List;
4 |
5 | import org.springframework.data.repository.CrudRepository;
6 | import org.springframework.stereotype.Repository;
7 |
8 | @Repository
9 | public interface BookRepository extends CrudRepository {
10 | List findByLibraryId(Integer libraryId);
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/repository/BookRepositoryProjector.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.repository;
2 |
3 | import java.util.List;
4 | import java.util.function.Function;
5 | import java.util.stream.Collectors;
6 |
7 | import org.axonframework.eventhandling.EventHandler;
8 | import org.axonframework.queryhandling.QueryHandler;
9 | import org.sgitario.axon.library.events.BookCreatedEvent;
10 | import org.sgitario.axon.library.models.BookBean;
11 | import org.sgitario.axon.library.queries.GetBooksQuery;
12 | import org.springframework.stereotype.Service;
13 |
14 | @Service
15 | public class BookRepositoryProjector {
16 |
17 | private final BookRepository bookRepository;
18 |
19 | public BookRepositoryProjector(BookRepository bookRepository) {
20 | this.bookRepository = bookRepository;
21 | }
22 |
23 | @EventHandler
24 | public void addBook(BookCreatedEvent event) throws Exception {
25 | BookEntity book = new BookEntity();
26 | book.setIsbn(event.getIsbn());
27 | book.setLibraryId(event.getLibraryId());
28 | book.setTitle(event.getTitle());
29 | bookRepository.save(book);
30 | }
31 |
32 | @QueryHandler
33 | public List getBooks(GetBooksQuery query) {
34 | return bookRepository.findByLibraryId(query.getLibraryId()).stream().map(toBook()).collect(Collectors.toList());
35 | }
36 |
37 | private Function toBook() {
38 | return e -> {
39 | BookBean book = new BookBean();
40 | book.setIsbn(e.getIsbn());
41 | book.setTitle(e.getTitle());
42 | return book;
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/org/sgitario/axon/library/repository/LibraryProjector.java:
--------------------------------------------------------------------------------
1 | package org.sgitario.axon.library.repository;
2 |
3 | import java.util.concurrent.CompletableFuture;
4 | import java.util.concurrent.ExecutionException;
5 |
6 | import org.axonframework.modelling.command.Repository;
7 | import org.axonframework.queryhandling.QueryHandler;
8 | import org.sgitario.axon.library.aggregate.Library;
9 | import org.sgitario.axon.library.queries.GetLibraryQuery;
10 | import org.springframework.stereotype.Service;
11 |
12 | @Service
13 | public class LibraryProjector {
14 | private final Repository libraryRepository;
15 |
16 | public LibraryProjector(Repository libraryRepository) {
17 | this.libraryRepository = libraryRepository;
18 | }
19 |
20 | @QueryHandler
21 | public Library getLibrary(GetLibraryQuery query) throws InterruptedException, ExecutionException {
22 | CompletableFuture future = new CompletableFuture();
23 | libraryRepository.load("" + query.getLibraryId()).execute(future::complete);
24 | return future.get();
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------