Code Monkey home page Code Monkey logo

library's Introduction

CircleCI Code Coverage

Table of contents

  1. About
  2. Domain description
  3. General assumptions
    3.1 Process discovery
    3.2 Project structure and architecture
    3.3 Aggregates
    3.4 Events
    3.4.1 Events in Repositories
    3.5 ArchUnit
    3.6 Functional thinking
    3.7 No ORM
    3.8 Architecture-code gap
    3.9 Model-code gap
    3.10 Spring
    3.11 Tests
  4. How to contribute
  5. References

About

This is a project of a library, driven by real business requirements. We use techniques strongly connected with Domain Driven Design, Behavior-Driven Development, Event Storming, User Story Mapping.

Domain description

A public library allows patrons to place books on hold at its various library branches. Available books can be placed on hold only by one patron at any given point in time. Books are either circulating or restricted, and can have retrieval or usage fees. A restricted book can only be held by a researcher patron. A regular patron is limited to five holds at any given moment, while a researcher patron is allowed an unlimited number of holds. An open-ended book hold is active until the patron checks out the book, at which time it is completed. A closed-ended book hold that is not completed within a fixed number of days after it was requested will expire. This check is done at the beginning of a day by taking a look at daily sheet with expiring holds. Only a researcher patron can request an open-ended hold duration. Any patron with more than two overdue checkouts at a library branch will get a rejection if trying a hold at that same library branch. A book can be checked out for up to 60 days. Check for overdue checkouts is done by taking a look at daily sheet with overdue checkouts. Patron interacts with his/her current holds, checkouts, etc. by taking a look at patron profile. Patron profile looks like a daily sheet, but the information there is limited to one patron and is not necessarily daily. Currently a patron can see current holds (not canceled nor expired) and current checkouts (including overdue). Also, he/she is able to hold a book and cancel a hold.

How actually a patron knows which books are there to lend? Library has its catalogue of books where books are added together with their specific instances. A specific book instance of a book can be added only if there is book with matching ISBN already in the catalogue. Book must have non-empty title and price. At the time of adding an instance we decide whether it will be Circulating or Restricted. This enables us to have book with same ISBN as circulated and restricted at the same time (for instance, there is a book signed by the author that we want to keep as Restricted)

General assumptions

Process discovery

The first thing we started with was domain exploration with the help of Big Picture EventStorming. The description you found in the previous chapter, landed on our virtual wall:
Event Storming Domain description
The EventStorming session led us to numerous discoveries, modeled with the sticky notes:
Event Storming Big Picture
During the session we discovered following definitions:
Event Storming Definitions

This made us think of real life scenarios that might happen. We discovered them described with the help of the Example mapping:
Example mapping

This in turn became the base for our Design Level sessions, where we analyzed each example:
Example mapping

Please follow the links below to get more details on each of the mentioned steps:

Project structure and architecture

At the very beginning, not to overcomplicate the project, we decided to assign each bounded context to a separate package, which means that the system is a modular monolith. There are no obstacles, though, to put contexts into maven modules or finally into microservices.

Bounded contexts should (amongst others) introduce autonomy in the sense of architecture. Thus, each module encapsulating the context has its own local architecture aligned to problem complexity. In the case of a context, where we identified true business logic (lending) we introduced a domain model that is a simplified (for the purpose of the project) abstraction of the reality and utilized hexagonal architecture. In the case of a context, that during Event Storming turned out to lack any complex domain logic, we applied CRUD-like local architecture.

Architecture

If we are talking about hexagonal architecture, it lets us separate domain and application logic from frameworks (and infrastructure). What do we gain with this approach? Firstly, we can unit test most important part of the application - business logic - usually without the need to stub any dependency. Secondly, we create ourselves an opportunity to adjust infrastructure layer without the worry of breaking the core functionality. In the infrastructure layer we intensively use Spring Framework as probably the most mature and powerful application framework with an incredible test support. More information about how we use Spring you will find here.

As we already mentioned, the architecture was driven by Event Storming sessions. Apart from identifying contexts and their complexity, we could also make a decision that we separate read and write models (CQRS). As an example you can have a look at Patron Profiles and Daily Sheets.

Aggregates

Aggregates discovered during Event Storming sessions communicate with each other with events. There is a contention, though, should they be consistent immediately or eventually? As aggregates in general determine business boundaries, eventual consistency sounds like a better choice, but choices in software are never costless. Providing eventual consistency requires some infrastructural tools, like message broker or event store. That's why we could (and did) start with immediate consistency.

Good architecture is the one which postpones all important decisions

... that's why we made it easy to change the consistency model, providing tests for each option, including basic implementations based on DomainEvents interface, which can be adjusted to our needs and toolset in future. Let's have a look at following examples:

  • Immediate consistency

    def 'should synchronize Patron, Book and DailySheet with events'() {
        given:
            bookRepository.save(book)
        and:
            patronRepo.publish(patronCreated())
        when:
            patronRepo.publish(placedOnHold(book))
        then:
            patronShouldBeFoundInDatabaseWithOneBookOnHold(patronId)
        and:
            bookReactedToPlacedOnHoldEvent()
        and:
            dailySheetIsUpdated()
    }
    
    boolean bookReactedToPlacedOnHoldEvent() {
        return bookRepository.findBy(book.bookId).get() instanceof BookOnHold
    }
    
    boolean dailySheetIsUpdated() {
        return new JdbcTemplate(datasource).query("select count(*) from holds_sheet s where s.hold_by_patron_id = ?",
                [patronId.patronId] as Object[],
                new ColumnMapRowMapper()).get(0)
                .get("COUNT(*)") == 1
    }

    Please note that here we are just reading from database right after events are being published

    Simple implementation of the event bus is based on Spring application events:

    @AllArgsConstructor
    public class JustForwardDomainEventPublisher implements DomainEvents {
    
        private final ApplicationEventPublisher applicationEventPublisher;
    
        @Override
        public void publish(DomainEvent event) {
            applicationEventPublisher.publishEvent(event);
        }
    }
  • Eventual consistency

    def 'should synchronize Patron, Book and DailySheet with events'() {
        given:
            bookRepository.save(book)
        and:
            patronRepo.publish(patronCreated())
        when:
            patronRepo.publish(placedOnHold(book))
        then:
            patronShouldBeFoundInDatabaseWithOneBookOnHold(patronId)
        and:
            bookReactedToPlacedOnHoldEvent()
        and:
            dailySheetIsUpdated()
    }
    
    void bookReactedToPlacedOnHoldEvent() {
        pollingConditions.eventually {
            assert bookRepository.findBy(book.bookId).get() instanceof BookOnHold
        }
    }
    
    void dailySheetIsUpdated() {
        pollingConditions.eventually {
            assert countOfHoldsInDailySheet() == 1
        }
    }

    Please note that the test looks exactly the same as previous one, but now we utilized Groovy's PollingConditions to perform asynchronous functionality tests

    Sample implementation of event bus is following:

    @AllArgsConstructor
    public class StoreAndForwardDomainEventPublisher implements DomainEvents {
    
        private final JustForwardDomainEventPublisher justForwardDomainEventPublisher;
        private final EventsStorage eventsStorage;
    
        @Override
        public void publish(DomainEvent event) {
            eventsStorage.save(event);
        }
    
        @Scheduled(fixedRate = 3000L)
        @Transactional
        public void publishAllPeriodically() {
            List<DomainEvent> domainEvents = eventsStorage.toPublish();
            domainEvents.forEach(justForwardDomainEventPublisher::publish);
            eventsStorage.published(domainEvents);
        }
    }

To clarify, we should always aim for aggregates that can handle a business operation atomically (transactionally if you like), so each aggregate should be as independent and decoupled from other aggregates as possible. Thus, eventual consistency is promoted. As we already mentioned, it comes with some tradeoffs, so from the pragmatic point of view immediate consistency is also a choice. You might ask yourself a question now: What if I don't have any events yet?. Well, a pragmatic approach would be to encapsulate the communication between aggregates in a Service-like class, where you could call proper aggregates line by line explicitly.

Events

Talking about inter-aggregate communication, we must remember that events reduce coupling, but don't remove it completely. Thus, it is very vital to share(publish) only those events, that are necessary for other aggregates to exist and function. Otherwise there is a threat that the level of coupling will increase introducing feature envy, because other aggregates might start using those events to perform actions they are not supposed to perform. A solution to this problem could be the distinction of domain events and integration events, which will be described here soon.

Events in Repositories

Repositories are one of the most popular design pattern. They abstract our domain model from data layer. In other words, they deal with state. That said, a common use-case is when we pass a new state to our repository, so that it gets persisted. It may look like so:

public class BusinessService {
   
    private final PatronRepository patronRepository;
    
    void businessMethod(PatronId patronId) {
        Patron patron = patronRepository.findById(patronId);
        //do sth
        patronRepository.save(patron);
    }
}

Conceptually, between 1st and 3rd line of that business method we change state of our Patron from A to B. This change might be calculated by dirty checking or we might just override entire Patron state in the database. Third option is Let's make implicit explicit and actually call this state change A->B an event. After all, event-driven architecture is all about promoting state changes as domain events.

Thanks to this our domain model may become immutable and just return events as results of invoking a command like so:

public BookPlacedOnHold placeOnHold(AvailableBook book) {
      ...
}

And our repository might operate directly on events like so:

public interface PatronRepository {
     void save(PatronEvent event) {
}

ArchUnit

One of the main components of a successful project is technical leadership that lets the team go in the right direction. Nevertheless, there are tools that can support teams in keeping the code clean and protect the architecture, so that the project won't become a Big Ball of Mud, and thus will be pleasant to develop and to maintain. The first option, the one we proposed, is ArchUnit - a Java architecture test tool. ArchUnit lets you write unit tests of your architecture, so that it is always consistent with initial vision. Maven modules could be an alternative as well, but let's focus on the former.

In terms of hexagonal architecture, it is essential to ensure, that we do not mix different levels of abstraction (hexagon levels):

@ArchTest
public static final ArchRule model_should_not_depend_on_infrastructure =
    noClasses()
        .that()
        .resideInAPackage("..model..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..infrastructure..");

and that frameworks do not affect the domain model

@ArchTest
public static final ArchRule model_should_not_depend_on_spring =
    noClasses()
        .that()
        .resideInAPackage("..io.pillopl.library.lending..model..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("org.springframework..");

Functional thinking

When you look at the code you might find a scent of functional programming. Although we do not follow a clean FP, we try to think of business processes as pipelines or workflows, utilizing functional style through following concepts.

Please note that this is not a reference project for FP.

Immutable objects

Each class that represents a business concept is immutable, thanks to which we:

  • provide full encapsulation and objects' states protection,
  • secure objects for multithreaded access,
  • control all side effects much clearer.

Pure functions

We model domain operations, discovered in Design Level Event Storming, as pure functions, and declare them in both domain and application layers in the form of Java's functional interfaces. Their implementations are placed in infrastructure layer as ordinary methods with side effects. Thanks to this approach we can follow the abstraction of ubiquitous language explicitly, and keep this abstraction implementation-agnostic. As an example, you could have a look at FindAvailableBook interface and its implementation:

@FunctionalInterface
public interface FindAvailableBook {

    Option<AvailableBook> findAvailableBookBy(BookId bookId);
}
@AllArgsConstructor
class BookDatabaseRepository implements FindAvailableBook {

    private final JdbcTemplate jdbcTemplate;

    @Override
    public Option<AvailableBook> findAvailableBookBy(BookId bookId) {
        return Match(findBy(bookId)).of(
                Case($Some($(instanceOf(AvailableBook.class))), Option::of),
                Case($(), Option::none)
        );
    }  

    Option<Book> findBy(BookId bookId) {
        return findBookById(bookId)
                .map(BookDatabaseEntity::toDomainModel);
    }

    private Option<BookDatabaseEntity> findBookById(BookId bookId) {
        return Try
                .ofSupplier(() -> of(jdbcTemplate.queryForObject("SELECT b.* FROM book_database_entity b WHERE b.book_id = ?",
                                      new BeanPropertyRowMapper<>(BookDatabaseEntity.class), bookId.getBookId())))
                .getOrElse(none());
    }  
} 

Type system

Type system - like modelling - we modelled each domain object's state discovered during EventStorming as separate classes: AvailableBook, BookOnHold, CheckedOutBook. With this approach we provide much clearer abstraction than having a single Book class with an enum-based state management. Moving the logic to these specific classes brings Single Responsibility Principle to a different level. Moreover, instead of checking invariants in every business method we leave the role to the compiler. As an example, please consider following scenario: you can place on hold only a book that is currently available. We could have done it in a following way:

public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(Book book) {
  if (book.status == AVAILABLE) {  
      ...
  }
}

but we use the type system and declare method of following signature

public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
      ...
}

The more errors we discover at compile time the better.

Yet another advantage of applying such type system is that we can represent business flows and state transitions with functions much easier. As an example, following functions:

placeOnHold: AvailableBook -> BookHoldFailed | BookPlacedOnHold
cancelHold: BookOnHold -> BookHoldCancelingFailed | BookHoldCanceled

are much more concise and descriptive than these:

placeOnHold: Book -> BookHoldFailed | BookPlacedOnHold
cancelHold: Book -> BookHoldCancelingFailed | BookHoldCanceled

as here we have a lot of constraints hidden within function implementations.

Moreover if you think of your domain as a set of operations (functions) that are being executed on business objects (aggregates) you don't think of any execution model (like async processing). It is fine, because you don't have to. Domain functions are free from I/O operations, async, and other side-effects-prone things, which are put into the infrastructure layer. Thanks to this, we can easily test them without mocking mentioned parts.

Monads

Business methods might have different results. One might return a value or a null, throw an exception when something unexpected happens or just return different objects under different circumstances. All those situations are typical to object-oriented languages like Java, but do not fit into functional style. We are dealing with this issues with monads (monadic containers provided by Vavr):

  • When a method returns optional value, we use the Option monad:

    Option<Book> findBy(BookId bookId) {
        ...
    }
  • When a method might return one of two possible values, we use the Either monad:

    Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
        ...
    }
  • When an exception might occur, we use Try monad:

    Try<Result> placeOnHold(@NonNull PlaceOnHoldCommand command) {
        ...
    }

Thanks to this, we can follow the functional programming style, but we also enrich our domain language and make our code much more readable for the clients.

Pattern Matching

Depending on a type of a given book object we often need to perform different actions. Series of if/else or switch/case statements could be a choice, but it is the pattern matching that provides the most conciseness and flexibility. With the code like below we can check numerous patterns against objects and access their constituents, so our code has a minimal dose of language-construct noise:

private Book handleBookPlacedOnHold(Book book, BookPlacedOnHold bookPlacedOnHold) {
    return API.Match(book).of(
        Case($(instanceOf(AvailableBook.class)), availableBook -> availableBook.handle(bookPlacedOnHold)),
        Case($(instanceOf(BookOnHold.class)), bookOnHold -> raiseDuplicateHoldFoundEvent(bookOnHold, bookPlacedOnHold)),
        Case($(), () -> book)
    );
}

(No) ORM

If you run mvn dependency:tree you won't find any JPA implementation. Although we think that ORM solutions (like Hibernate) are very powerful and useful, we decided not to use them, as we wouldn't utilize their features. What features are talking about? Lazy loading, caching, dirty checking. Why don't we need them? We want to have more control over SQL queries and minimize the object-relational impedance mismatch ourselves. Moreover, thanks to relatively small aggregates, containing as little data as it is required to protect the invariants, we don't need the lazy loading mechanism either. With Hexagonal Architecture we have the ability to separate domain and persistence models and test them independently. Moreover, we can also introduce different persistence strategies for different aggregates. In this project, we utilize both plain SQL queries and JdbcTemplate and use new and very promising project called Spring Data JDBC, that is free from the JPA-related overhead mentioned before. Please find below an example of a repository:

interface PatronEntityRepository extends CrudRepository<PatronDatabaseEntity, Long> {

    @Query("SELECT p.* FROM patron_database_entity p where p.patron_id = :patronId")
    PatronDatabaseEntity findByPatronId(@Param("patronId") UUID patronId);

}

At the same time we propose other way of persisting aggregates, with plain SQL queries and JdbcTemplate:

@AllArgsConstructor
class BookDatabaseRepository implements BookRepository, FindAvailableBook, FindBookOnHold {

    private final JdbcTemplate jdbcTemplate;

    @Override
    public Option<Book> findBy(BookId bookId) {
        return findBookById(bookId)
                .map(BookDatabaseEntity::toDomainModel);
    }

    private Option<BookDatabaseEntity> findBookById(BookId bookId) {
        return Try
                .ofSupplier(() -> of(jdbcTemplate.queryForObject("SELECT b.* FROM book_database_entity b WHERE b.book_id = ?",
                                     new BeanPropertyRowMapper<>(BookDatabaseEntity.class), bookId.getBookId())))
                .getOrElse(none());
    }
    
    ...
}

Please note that despite having the ability to choose different persistence implementations for aggregates it is recommended to stick to one option within the app/team

Architecture-code gap

We put a lot of attention to keep the consistency between the overall architecture (including diagrams) and the code structure. Having identified bounded contexts we could organize them in modules (packages, to be more specific). Thanks to this we gain the famous microservices' autonomy, while having a monolithic application. Each package has well defined public API, encapsulating all implementation details by using package-protected or private scopes.

Just by looking at the package structure:

└── library
    ├── catalogue
    ├── commons
    │   ├── aggregates
    │   ├── commands
    │   └── events
    │       └── publisher
    └── lending
        ├── book
        │   ├── application
        │   ├── infrastructure
        │   └── model
        ├── dailysheet
        │   ├── infrastructure
        │   └── model
        ├── librarybranch
        │   └── model
        ├── patron
        │   ├── application
        │   ├── infrastructure
        │   └── model
        └── patronprofile
            ├── infrastructure
            ├── model
            └── web

you can see that the architecture is screaming that it has two bounded contexts: catalogue and lending. Moreover, the lending context is built around five business objects: book, dailysheet, librarybranch, patron, and patronprofile, while catalogue has no subpackages, which suggests that it might be a CRUD with no complex logic inside. Please find the architecture diagram below.

Component diagram

Yet another advantage of this approach comparing to packaging by layer for example is that in order to deliver a functionality you would usually need to do it in one package only, which is the aforementioned autonomy. This autonomy, then, could be transferred to the level of application as soon as we split our context-packages into separate microservices. Following this considerations, autonomy can be given away to a product team that can take care of the whole business area end-to-end.

Model-code gap

In our project we do our best to reduce model-code gap to bare minimum. It means we try to put equal attention to both the model and the code and keep them consistent. Below you will find some examples.

Placing on hold

Placing on hold

Starting with the easiest part, below you will find the model classes corresponding to depicted command and events:

@Value
class PlaceOnHoldCommand {
    ...
}
@Value
class BookPlacedOnHold implements PatronEvent {
    ...
}
@Value
class MaximumNumberOfHoldsReached implements PatronEvent {
    ...    
}
@Value
class BookHoldFailed implements PatronEvent {
    ...
}

We know it might not look impressive now, but if you have a look at the implementation of an aggregate, you will see that the code reflects not only the aggregate name, but also the whole scenario of PlaceOnHold command handling. Let us uncover the details:

public class Patron {

    public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
        return placeOnHold(book, HoldDuration.openEnded());
    }
    
    ...
}    

The signature of placeOnHold method screams, that it is possible to place a book on hold only when it is available (more information about protecting invariants by compiler you will find in Type system section). Moreover, if you try to place available book on hold it can either fail (BookHoldFailed) or produce some events - what events?

@Value
class BookPlacedOnHoldEvents implements PatronEvent {
    @NonNull UUID eventId = UUID.randomUUID();
    @NonNull UUID patronId;
    @NonNull BookPlacedOnHold bookPlacedOnHold;
    @NonNull Option<MaximumNumberOfHoldsReached> maximumNumberOfHoldsReached;

    @Override
    public Instant getWhen() {
        return bookPlacedOnHold.when;
    }

    public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold) {
        return new BookPlacedOnHoldEvents(bookPlacedOnHold.getPatronId(), bookPlacedOnHold, Option.none());
    }

    public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold, MaximumNumberOfHoldsReached maximumNumberOfHoldsReached) {
        return new BookPlacedOnHoldEvents(bookPlacedOnHold.patronId, bookPlacedOnHold, Option.of(maximumNumberOfHoldsReached));
    }

    public List<DomainEvent> normalize() {
        return List.<DomainEvent>of(bookPlacedOnHold).appendAll(maximumNumberOfHoldsReached.toList());
    }
}

BookPlacedOnHoldEvents is a container for BookPlacedOnHold event, and - if patron has 5 book placed on hold already - MaximumNumberOfHoldsReached (please mind the Option monad). You can see now how perfectly the code reflects the model.

It is not everything, though. In the picture above you can also see a big rectangular yellow card with rules (policies) that define the conditions that need to be fulfilled in order to get the given result. All those rules are implemented as functions either allowing or rejecting the hold:

Restricted book policy

PlacingOnHoldPolicy onlyResearcherPatronsCanHoldRestrictedBooksPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
    if (toHold.isRestricted() && patron.isRegular()) {
        return left(Rejection.withReason("Regular patrons cannot hold restricted books"));
    }
    return right(new Allowance());
};

Overdue checkouts policy

PlacingOnHoldPolicy overdueCheckoutsRejectionPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
    if (patron.overdueCheckoutsAt(toHold.getLibraryBranch()) >= OverdueCheckouts.MAX_COUNT_OF_OVERDUE_RESOURCES) {
        return left(Rejection.withReason("cannot place on hold when there are overdue checkouts"));
    }
    return right(new Allowance());
};

Max number of holds policy

PlacingOnHoldPolicy regularPatronMaximumNumberOfHoldsPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
    if (patron.isRegular() && patron.numberOfHolds() >= PatronHolds.MAX_NUMBER_OF_HOLDS) {
        return left(Rejection.withReason("patron cannot hold more books"));
    }
    return right(new Allowance());
};

Open ended hold policy

PlacingOnHoldPolicy onlyResearcherPatronsCanPlaceOpenEndedHolds = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
    if (patron.isRegular() && holdDuration.isOpenEnded()) {
        return left(Rejection.withReason("regular patron cannot place open ended holds"));
    }
    return right(new Allowance());
};

Spring

Spring Framework seems to be the most popular Java framework ever used. Unfortunately it is also quite common to overuse its features in the business code. What you find in this project is that the domain packages are fully focused on modelling business problems, and are free from any DI, which makes it easy to unit-test it which is invaluable in terms of code reliability and maintainability. It does not mean, though, that we do not use Spring Framework - we do. Below you will find some details:

  • Each bounded context has its own independent application context. It means that we removed the runtime coupling, which is a step towards extracting modules (and microservices). How did we do that? Let's have a look:
    @SpringBootConfiguration
    @EnableAutoConfiguration
    public class LibraryApplication {
    
        public static void main(String[] args) {
            new SpringApplicationBuilder()
                    .parent(LibraryApplication.class)
                    .child(LendingConfig.class).web(WebApplicationType.SERVLET)
                    .sibling(CatalogueConfiguration.class).web(WebApplicationType.NONE)
                    .run(args);
        }
    }
  • As you could see above, we also try not to use component scan wherever possible. Instead we utilize @Configuration classes where we define module specific beans in the infrastructure layer. Those configuration classes are explicitly declared in the main application class.

Tests

Tests are written in a BDD manner, expressing stories defined with Example Mapping. It means we utilize both TDD and Domain Language discovered with Event Storming.

We also made an effort to show how to create a DSL, that enables to write tests as if they were sentences taken from the domain descriptions. Please find an example below:

def 'should make book available when hold canceled'() {
    given:
        BookDSL bookOnHold = aCirculatingBook() with anyBookId() locatedIn anyBranch() placedOnHoldBy anyPatron()
    and:
        PatronEvent.BookHoldCanceled bookHoldCanceledEvent = the bookOnHold isCancelledBy anyPatron()

    when:
        AvailableBook availableBook = the bookOnHold reactsTo bookHoldCanceledEvent
    then:
        availableBook.bookId == bookOnHold.bookId
        availableBook.libraryBranch == bookOnHold.libraryBranchId
        availableBook.version == bookOnHold.version
}

Please also note the when block, where we manifest the fact that books react to cancellation event

How to contribute

The project is still under construction, so if you like it enough to collaborate, just let us know or simply create a Pull Request.

How to Build

Requirements

  • Java 11
  • Maven

Quickstart

You can run the library app by simply typing the following:

$ mvn spring-boot:run
...
...
2019-04-03 15:55:39.162  INFO 18957 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2019-04-03 15:55:39.425  INFO 18957 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-03 15:55:39.428  INFO 18957 --- [           main] io.pillopl.library.LibraryApplication    : Started LibraryApplication in 5.999 seconds (JVM running for 23.018)

Build a Jar package

You can build a jar with maven like so:

$ mvn clean package
...
...
[INFO] Building jar: /home/pczarkowski/development/spring/library/target/library-0.0.1-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Build with Docker

If you've already built the jar file you can run:

docker build -t spring/library .

Otherwise you can build the jar file using the multistage dockerfile:

docker build -t spring/library -f Dockerfile.build .

Either way once built you can run it like so:

$ docker run -ti --rm --name spring-library -p 8080:8080 spring/library

Production ready metrics and visualization

To run the application as well as Prometheus and Grafana dashboard for visualizing metrics you can run all services:

$ docker-compose up

If everything goes well, you can access the following services at given location:

In order to see some metrics, you must create a dashboard. Go to Create -> Import and select attached jvm-micrometer_rev8.json. File has been pulled from https://grafana.com/grafana/dashboards/4701.

Please note application will be run with local Spring profile to setup some initial data.

References

  1. Introducing EventStorming by Alberto Brandolini
  2. Domain Modelling Made Functional by Scott Wlaschin
  3. Software Architecture for Developers by Simon Brown
  4. Clean Architecture by Robert C. Martin
  5. Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans

library's People

Contributors

ajurasz avatar amirdt22 avatar bslota avatar delor avatar ghisvail avatar jakzal avatar jklata avatar krzykrucz avatar lukaszkostrzewa avatar marcinswierczynski avatar marciovmartins avatar mszarlinski avatar paulczar avatar pillopl avatar pszymczyk avatar wyhasany avatar ziebamarcin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

library's Issues

GroovyTemplateAutoConfiguration - Cannot find template.

I cloned repository from master branch today.
I got a lot of logs saying:

14:20:40.556 [main] WARN o.s.b.a.g.t.GroovyTemplateAutoConfiguration - Cannot find template location: classpath:/templates/ (please add some templates, check your Groovy configuration, or set spring.groovy.template.check-template-location=false)

during integration tests.

Remove findBy from save at BookRepository.save(Book book)

 @Override
    public void save(Book book) {
        findBy(book.bookId())
                .map(entity -> updateOptimistically(book))
                .onEmpty(() -> insertNew(book));
    }

this findBy is only there to have updateOrCreate behavior. But it makes optimistic locking doesn't work. in FindBy we fetch new Version

Architecture hexagonal ?

Hi,

Thank you for your example.

I have remarks about your implementation of architecture hexagonal pattern.
In architecture hexagonal we have port and adapter.
In my understanding,
Port is in domain package and adapter in infrastructure package.
(Golden rule : Infra can see domain.
Domain shouldn't see infra.)

However in your package catalogue you have CatalogueDatabase (adapter) and
CatalogueConfiguration (infra) and Catalogue (domain).

Do you really follow archi hexagonal pattern or am I wrong somewhere ?

Data consistency

Hi, I've been studying this project for a while. I really like what you've done here.

I would like to hear advice on how to solve the next issue. The consistency problem be replicated with this steps:

  1. Patron tries to hold a book with the controller.
  2. Patron Aggregate validates the command and then creates a BookPlacedOnHold event.
  3. BookPlacedOnHold arrives to the PatronsDatabaseRepository. The adds the new hold to the patron and then publishes the event.
  4. The event is listen in the Book's PatronEventsHandler
    Here comes the problem:
    If, for some reason, the database fails to persist the Book the database will be at an inconsistent state.
    The Patron will have a Hold but the book state will be Available.

Thanks in advance!

Aggregate domain model differences - Patron vs Book

Hello,
I've a question for concepts of implementing aggregates in your code.
In the readme there is a statement:

our domain model may become immutable and just return events as results of invocking a command

And it's nice, but on the other hand Book aggregate looks quite different. The Patron aggregate state is modeled in one class, and the previous conception is easy acceptable in this situation. But the Book aggregate, as I see accepts events, and returns new states (in the future I see here opportunity for event sourcing with small effort).
It's also OK, but why those two aggregates are modeled in different manners?
And what about compensation process for event BookDuplicateHoldFound - it should be handled by Patron aggregate, shouldn't it? So in this case the Patron aggregate also ought to contains method which accepts event and return new state.

Food for thoughts about extensions

I just wondered about the two things:

  1. No context map. IMHO this would be helpful to get an overview of the logical relationship between the bounded contexts https://github.com/ddd-crew/context-mapping
  2. Especially since ArchUnit is already used, it would be a reasonable improvement to incorporate https://github.com/xmolecules/jmolecules to further formally codify and validate the architectural concepts

PS: I deliberately misused the Issues feature since the Discussions feature seems to be disabled https://docs.github.com/en/discussions

Book that is currently not available - alternative

"When a patron tires to place on hold a book that is currently not available it should not be possible, thus resulting in book hold failed event, as it is depicted below"

The book view should not let the patrol to hold only available books ?
(grey out the others etc)

In this way will be impossible to to hold a book that is not available ....

Cannot find Patron aggregate

Hello

On the very beginning many thanks for a great project!

I've just fetched repo into my machine and I have a problem to find Patron aggregate which is mention on project structure it's a little bit confusing. I think that more people would like to start learning domain from root aggregate and It would be also quite confusing for them.

Screenshot 2019-04-05 at 08 59 26

Domain description adding Polish translation

Hey,

i spent some time trying to understood in details what the domain is about.
I suggest to add additionally polish translation.

This is my attempt to do so:

A public library allows patrons to place books on hold at its various library branches. 
Publiczna biblioteka pozwala patronom zarezerwować książki w różnych bibliotecznych oddziałach.

Available books can be placed on hold only by one patron at any given point in time. 
Dostępne książki mogą być zarezerwowane tylko przez jednego patrona w dowolnym czasie.

Books are either circulating or restricted, and can have retrieval or usage fees. 
Książki są ogólnodostępne, albo mają ograniczony dostęp i mogą mieć koszt za zwrot, czy użytkowanie.

A restricted book can only be held by a researcher patron.
Książki o ograniczonym dostępie mogą być zarezerwowane tylko przez patrona badacza. 

 A regular patron is limited to five holds at any given moment, while a researcher patron is allowed an unlimited number of holds.

 Zwykły patron jest ograniczony limitem ilości rezerwacji, wynoszącym pięć książek w dowolnym momencie, podczas gdy
 patron badacz może rezerwować ile chce.

 An open-ended book hold is active until the patron checks out the book, at which time it is completed.
 Rezerwacja książki na czas nieokreślony jest ważna dopóki patron jej nie odbierze (w chwili odebrania rezerwacja jest zakończona).

 A closed-ended book hold that is not completed within a fixed number of days after it was requested will expire.
 Rezerwacja książki na czas określony, która nie została zrealizowana po upływie ustalonego czasu traci ważność.

 This check is done at the beginning of a day by taking a look at daily sheet with expiring holds. 
 Sprawdzenie tego następuje na początku dnia - korzystając z arkusza zawierającego dane o przeterminowanych rezerwacjach.

 Only a researcher patron can request an open-ended hold duration. 
 Tylko patron badacz może zarezerwować książkę na czas nieokreślony.
 
 Any patron with more than two overdue checkouts at a library branch will get a rejection 
 if trying a hold at that same library branch. 
 Dowolny patron z więcej niż dwoma spóżnionymi odbiorami książek w danym oddziale bibliotecznym dostanie odmowe w przypadku, gdy
 będzie chciał zarezerwować książkę w tym samym oddziale bibliotecznym.

 A book can be checked out for up to 60 days.
 Książka może być wypożyczona na maksymalnie 60 dni.

 Check for overdue checkouts is done by taking a look at daily sheet with overdue checkouts. 
 Sprawdzenie przekroczonych czasowo zwrotów odbywa się poprzez korzystanie z dziennego arkusza z przekroczonymi czasowo zwrotami.

 Patron interacts with his/her current holds, checkouts, etc. by taking a look at patron profile. 
 Patron zarządza swoimi rezerwacjami, wypożyczeniami itp. - patrząc na swój profil patrona.

 Patron profile looks like a daily sheet, but the information there is limited to one patron and is not necessarily daily. 
 Profil patrona wygląda jak dzienny arkusz, ale informacje tam są ograniczone do jednego patrona i niekoniecznie dotyczą tylko jednego dnia.

 Currently a patron can see current holds (not canceled nor expired) and current checkouts (including overdue).
 Obecnie patron może zobaczyć aktualne rezerwacje (nie anulowane, ani nie te z utraconą ważnością)
 i aktualne wypożyczenia (włączając te przeterminowane). 

 Also, he/she is able to hold a book and cancel a hold.
 Dodatkowo on/ona mogą zarezerwować książkę i anulować rezerwację.

How actually a patron knows which books are there to lend? 
Skąd właściwie patron wie, które książki są do wypożyczenia?

Library has its catalogue of books where books are added together with their specific instances. 
Biblioteka ma swój katalog książek, gdzie książki są dodawane wraz z ich konkretnymi kopiami.

A specific book instance of a book can be added only if there is book with matching ISBN already in the catalogue. 
Konkretna kopia książki może zostać dodana tylko jeśli jest dostępna książka w katalogu z tym samym numerem ISBN.

Book must have non-empty title and price. 
Książka musi zawierać nie pusty tytuł i cene.

At the time of adding an instance we decide whether it will be Circulating or Restricted. 
Podczas dodawania kopii, decydujemy, czy będzie ona ogólnodostępna, czy o ograniczonym dostępie.

This enables us to have book with same ISBN as circulated and restricted at the same time 
(for instance, there is a book signed by the author that we want to keep as Restricted).
To pozwala nam mieć książkę z tym samym numerem ISBN jako zarazem ogólnodostępną jak i o ograniczonym dostępie (dla przykłada, jest książka podpisana przez autora, którą chcemy mieć z ograniczonym dostępem).

You can use it if you want to.

Question about packaging

Hey,

I am wondering is it better approach to use packaging by feature instead of packaging by layers using DDD.
In what direction your example packaging expand in the future?

Just by looking at the package structure:

└── library
    ├── catalogue
    ├── commons
    │   ├── aggregates
    │   ├── commands
    │   └── events
    │       └── publisher
    └── lending
        ├── book
        │   ├── application
        │   ├── infrastructure
        │   └── model
        ├── dailysheet
        │   ├── infrastructure
        │   └── model
        ├── librarybranch
        │   └── model
        ├── patron
        │   ├── application
        │   ├── infrastructure
        │   └── model
        └── patronprofile
            ├── infrastructure
            ├── model
            └── web

Does it start to use packaging by feature?

Not all tests are deterministic

I cloned repository from master branch today.
During mvn clean install I got once:

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR] FindingPatronProfileInDatabaseIT.should create patron profile:65->thereIsOnlyOneHold:87 Condition not satisfied:

profile.holdsView.currentHolds.get(0) == new Hold(bookId, TOMORROW)
| | | | | | | |
| | | | | | | 2019-12-17T13:17:58.435995800Z
| | | | | | BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945)
| | | | | Hold(book=BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945), till=2019-12-17T13:17:58.435995800Z)
| | | | false
| | | Hold(book=BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945), till=2019-12-17T13:17:58.435996Z)
| | List(Hold(book=BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945), till=2019-12-17T13:17:58.435996Z))
| HoldsView(currentHolds=List(Hold(book=BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945), till=2019-12-17T13:17:58.435996Z)))
PatronProfile(holdsView=HoldsView(currentHolds=List(Hold(book=BookId(bookId=53fbcaff-78d1-4159-8516-4fa6f5b8b945), till=2019-12-17T13:17:58.435996Z))), currentCheckouts=CheckoutsView(currentCheckouts=List()))

[INFO]
[ERROR] Tests run: 33, Failures: 1, Errors: 0, Skipped: 0

2nd, 3rd, etc. time I runned tests their were fine.

Update to spring boot 2.7.5 and Java 17

Hi, Thank you for sharing this project. This is a great example of DDD.

I tried to update the project to spring boot 2.7.5 as a transition to 3 but I ran into an issue with archunit.

io.pillopl.library.lending.architecture.NoSpringInDomainLogicTest is failing:

Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..io.pillopl.library.lending..application..' should depend on classes that reside in a package 'org.springframework..'' was violated (7 times):
Method <io.pillopl.library.lending.book.application.CreateAvailableBookOnInstanceAddedEventHandler.handle(io.pillopl.library.catalogue.BookInstanceAddedToCatalogue)> is annotated with <org.springframework.context.event.EventListener> in (CreateAvailableBookOnInstanceAddedEventHandler.java:0)
Method <io.pillopl.library.lending.book.application.PatronEventsHandler.handle(io.pillopl.library.lending.patron.model.PatronEvent$BookCheckedOut)> is annotated with <org.springframework.context.event.EventListener> in (PatronEventsHandler.java:0)
Method <io.pillopl.library.lending.book.application.PatronEventsHandler.handle(io.pillopl.library.lending.patron.model.PatronEvent$BookHoldCanceled)> is annotated with <org.springframework.context.event.EventListener> in (PatronEventsHandler.java:0)
Method <io.pillopl.library.lending.book.application.PatronEventsHandler.handle(io.pillopl.library.lending.patron.model.PatronEvent$BookHoldExpired)> is annotated with <org.springframework.context.event.EventListener> in (PatronEventsHandler.java:0)
Method <io.pillopl.library.lending.book.application.PatronEventsHandler.handle(io.pillopl.library.lending.patron.model.PatronEvent$BookPlacedOnHold)> is annotated with <org.springframework.context.event.EventListener> in (PatronEventsHandler.java:0)
Method <io.pillopl.library.lending.book.application.PatronEventsHandler.handle(io.pillopl.library.lending.patron.model.PatronEvent$BookReturned)> is annotated with <org.springframework.context.event.EventListener> in (PatronEventsHandler.java:0)
Method <io.pillopl.library.lending.patron.application.hold.HandleDuplicateHold.handle(io.pillopl.library.lending.book.model.BookDuplicateHoldFound)> is annotated with <org.springframework.context.event.EventListener> in (HandleDuplicateHold.java:0)

How should I handle this?

Question about aggregates

According to this: Business logic one aggregate can never depend on a state from another aggregate: why in the patron model you inject the book model?

[Question] hold_database_entity and overdue_checkout_database_entity table not used?

create_patron_db.sql contains DDL to create patron_database_entity, hold_database_entity and overdue_checkout_database_entity. But it seems the later two are never used. io.pillopl.library.lending.patron.infrastructure.PatronsDatabaseRepository just query from patron_database_entity without joining from the other two tables.

interface PatronEntityRepository extends CrudRepository<PatronDatabaseEntity, Long> {

    @Query("SELECT p.* FROM patron_database_entity p where p.patron_id = :patronId")
    PatronDatabaseEntity findByPatronId(@Param("patronId") UUID patronId);

}

Is the storage of the Book attributes redundant?

The values of some fields of BookDatabaseEntity are always the same, such as "available_at_branch" and "on_hold_at_branch" and "checked_out_at_branch", "on_hold_by_patron" and "checked_out_by_patron".

I can understand that these attributes have different meanings in the domain. but in terms of storage, is it feasible to map attributes with the same value to the same field, so that the number of fields can be reduced? For example, use "branch_id" and "patron_id".

class BookDatabaseEntity {
    UUID book_id;
    BookType book_type;
    BookState book_state;
    Instant on_hold_till;
    UUID branch_id;
    UUID patron_id;
    int version;
}

We can still judge the meaning of branch_id and patron_id through book_state.

[Question] What factors affected your design decision?

Hi,

I only discovered DDD a few hours back and this repo was the perfect head start. Which otherwise would have only been possible after days of reading through multiple books. I have a quick question though:-

I was looking at this scenario and I can see this was modeled as two sub-models:-

  1. Closed-ended book holding and
  2. Open-ended book holding

It is, however, not very obvious to me how someone arrived at this specific model when there could clearly have been other possibilities like:-

  1. Hold for Regular patrons and
  2. Hold for researchers

How can one eliminate all the other possible models and what tools/methods did you (can I) use to arrive at this design?

Thanks in advance!
-Koba

Hack, placeholder or mistake?

Class HoldsToExpireSheet has annotation @EventListeneron method Stream<PatronEvent.BookHoldExpired> toStreamOfEvents(). What is this? Is it a hack or just placeholder for future improvements or maybe just a mistake?

Wrong domain abstractions

CatalogueDatabase and BookInstanceAddedToCatalogue are not objects in the domain. Why are these included in your model?

Create concept of PublishedEvent

So currently the all of the domain events have information about business facts.

If we want to publish them we need to add metadata like:

causationID,
corellationID,
uniqueMessageID
etc

so PublishedEvent becomes DomainEvent + metadata

Test MeteredDomainEventPublisherSpec is not running during build.

I cloned repository from master branch today.

When running mvn clean install I don't see every test:

[INFO] --- maven-failsafe-plugin:2.22.1:integration-test (default) @ library ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.pillopl.library.catalogue.CatalogueDatabaseIT
(...)
[INFO] Running io.pillopl.library.lending.book.infrastructure.BookDatabaseRepositoryIT
(...)
[INFO] Running io.pillopl.library.lending.book.infrastructure.DuplicateHoldFoundIT
(...)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.468 s - in io.pillopl.library.lending.book.infrastructure.DuplicateHoldFoundIT
[INFO] Running io.pillopl.library.lending.book.infrastructure.FindAvailableBookInDatabaseIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s - in io.pillopl.library.lending.book.infrastructure.FindAvailableBookInDatabaseIT
[INFO] Running io.pillopl.library.lending.book.infrastructure.FindBookOnHoldInDatabaseIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 s - in io.pillopl.library.lending.book.infrastructure.FindBookOnHoldInDatabaseIT
[INFO] Running io.pillopl.library.lending.book.infrastructure.OptimisticLockingBookAggregateIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s - in io.pillopl.library.lending.book.infrastructure.OptimisticLockingBookAggregateIT
[INFO] Running io.pillopl.library.lending.dailysheet.infrastructure.FindingHoldsInDailySheetDatabaseIT
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.15 s - in io.pillopl.library.lending.dailysheet.infrastructure.FindingHoldsInDailySheetDatabaseIT
[INFO] Running io.pillopl.library.lending.dailysheet.infrastructure.FindingOverdueCheckoutsInDailySheetDatabaseIT
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 s - in io.pillopl.library.lending.dailysheet.infrastructure.FindingOverdueCheckoutsInDailySheetDatabaseIT
[INFO] Running io.pillopl.library.lending.eventspropagation.EventualConsistencyBetweenAggregatesAndReadModelsIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.697 s - in io.pillopl.library.lending.eventspropagation.EventualConsistencyBetweenAggregatesAndReadModelsIT
[INFO] Running io.pillopl.library.lending.eventspropagation.StrongConsistencyBetweenAggregatesAndReadModelsIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 s - in io.pillopl.library.lending.eventspropagation.StrongConsistencyBetweenAggregatesAndReadModelsIT
[INFO] Running io.pillopl.library.lending.patron.infrastructure.PatronDatabaseRepositoryIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s - in io.pillopl.library.lending.patron.infrastructure.PatronDatabaseRepositoryIT
[INFO] Running io.pillopl.library.lending.patronprofile.infrastructure.FindingPatronProfileInDatabaseIT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.036 s - in io.pillopl.library.lending.patronprofile.infrastructure.FindingPatronProfileInDatabaseIT
[INFO] Running io.pillopl.library.lending.patronprofile.web.PatronProfileControllerIT
(...)
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.455 s - in io.pillopl.library.lending.patronprofile.web.PatronProfileControllerIT
(...)
[INFO] Tests run: 33, Failures: 0, Errors: 0, Skipped: 0

There's log about BookDatabaseRepositoryIT whis is annoted with @SpringBootTest
but there's no log about MeteredDomainEventPublisherSpec test which is weird.

Changing MeteredDomainEventPublisherSpec test to something like:

        then:
            countedEvents("domain_events", "name", "TestEvent") == 12121
        when:
            publisher.publish(new TestEvent())
        then:
            countedEvents("domain_events", "name", "TestEvent") == 13131

Clearly proves that this test is not runned.

JdbcConfiguration is deprecated

JdbcConfiguration is used in LendingDatabaseConfig that has been deprecated and recommended to use AbstractJdbcConfiguration

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.