├── .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 | ![Axon Architecture]({{ site.url }}{{ site.baseurl }}/images/axon-architecture-1.png) 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 | ![Axon Dashboard]({{ site.url }}{{ site.baseurl }}/images/axon-installation-1.png) 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 | ![Diagram]({{ site.url }}{{ site.baseurl }}/images/axon-bookstore-diagram.jpg) 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 | --------------------------------------------------------------------------------