├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── ddd-to-the-code-workshop-accounting └── src │ ├── main │ ├── resources │ │ ├── schema.sql │ │ ├── static │ │ │ └── layout.css │ │ ├── application.properties │ │ └── templates │ │ │ └── list-wallets.html │ └── java │ │ └── com │ │ └── github │ │ └── cstettler │ │ └── dddttc │ │ └── accounting │ │ ├── domain │ │ ├── WalletRepository.java │ │ ├── WalletOwner.java │ │ ├── TransactionReference.java │ │ ├── WalletNotExistingException.java │ │ ├── WalletAlreadyExistsException.java │ │ ├── WalletInitializedEvent.java │ │ ├── BookingAlreadyBilledException.java │ │ ├── BookingId.java │ │ ├── Transaction.java │ │ ├── TransactionWithSameReferenceAlreadyAppliedException.java │ │ ├── Amount.java │ │ ├── BookingFeePolicy.java │ │ ├── Booking.java │ │ ├── ChargeWelcomeAmountToWalletService.java │ │ └── Wallet.java │ │ ├── infrastructure │ │ ├── event │ │ │ ├── UserRegistrationCompletedMessageListener.java │ │ │ └── BookingCompletedMessageListener.java │ │ ├── web │ │ │ └── AccountingController.java │ │ └── persistence │ │ │ └── JdbcWalletRepository.java │ │ ├── application │ │ └── WalletService.java │ │ └── AccountingApplication.java │ └── test │ ├── resources │ └── application-test.properties │ └── java │ └── com │ └── github │ └── cstettler │ └── dddttc │ └── accounting │ ├── AccountingApplicationTests.java │ ├── domain │ ├── WalletBuilder.java │ └── WalletMatcher.java │ └── infrastructure │ └── test │ ├── UserRegistrationCompletedMessageListenerTests.java │ └── WalletScenarioTests.java ├── docs └── bounded-context-and-use-cases.png ├── ddd-to-the-code-workshop-rental └── src │ ├── main │ ├── resources │ │ ├── static │ │ │ └── layout.css │ │ ├── schema.sql │ │ ├── application.properties │ │ └── templates │ │ │ ├── error.html │ │ │ ├── list-bikes.html │ │ │ ├── book-bike.html │ │ │ └── list-bookings.html │ └── java │ │ └── com │ │ └── github │ │ └── cstettler │ │ └── dddttc │ │ └── rental │ │ ├── domain │ │ ├── user │ │ │ ├── UserRepository.java │ │ │ ├── UserAlreadyExistsException.java │ │ │ ├── UserId.java │ │ │ ├── LastName.java │ │ │ ├── FirstName.java │ │ │ ├── UserNotExistingException.java │ │ │ └── User.java │ │ ├── bike │ │ │ ├── BikeRepository.java │ │ │ ├── NumberPlate.java │ │ │ ├── BikeNotExistingException.java │ │ │ ├── BikeAlreadyBookedException.java │ │ │ ├── ReleaseBikeService.java │ │ │ ├── InitializeBikesService.java │ │ │ └── Bike.java │ │ └── booking │ │ │ ├── BookingRepository.java │ │ │ ├── BookingNotExistingException.java │ │ │ ├── BookingAlreadyCompletedException.java │ │ │ ├── BookingId.java │ │ │ ├── BikeUsage.java │ │ │ ├── BookingCompletedEvent.java │ │ │ ├── BookBikeService.java │ │ │ └── Booking.java │ │ ├── application │ │ ├── BikeService.java │ │ ├── UserService.java │ │ └── BookingService.java │ │ ├── infrastructure │ │ ├── bootstrap │ │ │ └── BikeInitializationTrigger.java │ │ ├── persistence │ │ │ ├── InMemoryBikeRepository.java │ │ │ ├── JdbcUserRepository.java │ │ │ └── JdbcBookingRepository.java │ │ └── event │ │ │ └── UserRegistrationCompletedMessageListener.java │ │ └── RentalApplication.java │ └── test │ └── java │ └── com │ └── github │ └── cstettler │ └── dddttc │ └── rental │ └── RentalApplicationTests.java ├── ddd-to-the-code-workshop-registration └── src │ ├── main │ ├── resources │ │ ├── static │ │ │ └── layout.css │ │ ├── schema.sql │ │ ├── application.properties │ │ └── templates │ │ │ ├── error.html │ │ │ ├── done.html │ │ │ ├── start.html │ │ │ ├── verify.html │ │ │ └── complete.html │ └── java │ │ └── com │ │ └── github │ │ └── cstettler │ │ └── dddttc │ │ └── registration │ │ ├── domain │ │ ├── SmsNotificationSender.java │ │ ├── UserHandle.java │ │ ├── support │ │ │ ├── Validations.java │ │ │ └── MandatoryParameterMissingException.java │ │ ├── PhoneNumberNotSwissException.java │ │ ├── UserRegistrationRepository.java │ │ ├── UserHandleAlreadyInUseException.java │ │ ├── PhoneNumberNotYetVerifiedException.java │ │ ├── PhoneNumberAlreadyVerifiedException.java │ │ ├── PhoneNumber.java │ │ ├── FullName.java │ │ ├── UserRegistrationNotExistingException.java │ │ ├── PhoneNumberVerificationCodeInvalidException.java │ │ ├── UserRegistrationAlreadyCompletedException.java │ │ ├── UserRegistrationId.java │ │ ├── SendVerificationCodeSmsService.java │ │ ├── PhoneNumberVerificationCodeGeneratedEvent.java │ │ ├── VerificationCode.java │ │ └── UserRegistrationCompletedEvent.java │ │ ├── infrastructure │ │ └── sms │ │ │ └── LoggingSmsNotificationSender.java │ │ ├── RegistrationApplication.java │ │ └── application │ │ └── UserRegistrationService.java │ └── test │ ├── resources │ └── application-test.properties │ └── java │ └── com │ └── github │ └── cstettler │ └── dddttc │ └── registration │ ├── RegistrationApplicationTests.java │ └── domain │ ├── UserHandleTests.java │ ├── VerificationCodeTests.java │ ├── PhoneNumberTests.java │ ├── UserRegistrationCompletedEventMatcher.java │ ├── SendVerificationCodeSmsServiceTests.java │ ├── UserRegistrationIdTests.java │ └── UserRegistrationMatcher.java ├── ddd-to-the-code-workshop-support └── src │ ├── test │ ├── resources │ │ ├── application-test.properties │ │ └── logback-test.xml │ └── java │ │ └── com │ │ └── github │ │ └── cstettler │ │ └── dddttc │ │ └── support │ │ ├── infrastructure │ │ └── event │ │ │ ├── TestDomainEvent.java │ │ │ ├── TestDomainService.java │ │ │ ├── TestApplication.java │ │ │ └── DomainEventSupportTests.java │ │ ├── test │ │ ├── ScenarioTest.java │ │ └── DatabaseCleaner.java │ │ ├── domain │ │ └── RecordingDomainEventPublisher.java │ │ ├── EnableComponentScanExclusions.java │ │ ├── ReflectionUtils.java │ │ ├── ReflectionBasedStateBuilder.java │ │ └── ReflectionBasedStateMatcher.java │ └── main │ ├── resources │ └── schema.sql │ └── java │ └── com │ └── github │ └── cstettler │ └── dddttc │ └── support │ ├── domain │ └── DomainEventPublisher.java │ ├── RepositoryImplementationFilter.java │ ├── InfrastructureServiceImplementationFilter.java │ └── infrastructure │ ├── event │ ├── PendingDomainEvent.java │ ├── DomainEventTypeResolver.java │ ├── DomainEventTypeMappings.java │ ├── DomainEventSerializer.java │ ├── DomainEventHandlerAnnotationBeanPostProcessor.java │ ├── PendingDomainEventStore.java │ ├── DomainEventSupportConfiguration.java │ ├── TransactionalEnqueuingDomainEventPublisher.java │ └── PendingDomainEventPublisher.java │ ├── web │ └── ModelAndViewBuilder.java │ └── persistence │ └── JsonBasedPersistenceSupport.java ├── ddd-to-the-code-workshop-stereotype ├── src │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── cstettler │ │ └── dddttc │ │ └── stereotype │ │ ├── BusinessException.java │ │ ├── ValueObject.java │ │ ├── AggregateId.java │ │ ├── DomainService.java │ │ ├── Aggregate.java │ │ ├── ApplicationService.java │ │ ├── Repository.java │ │ ├── InfrastructureService.java │ │ ├── DomainEventHandler.java │ │ ├── AggregateFactory.java │ │ └── DomainEvent.java └── pom.xml ├── etc └── architecture-governance │ ├── index.adoc │ ├── reports.adoc │ └── constraints.adoc ├── ddd-to-the-code-workshop-aspect ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── github │ └── cstettler │ └── dddttc │ └── aspect │ ├── StateBasedEqualsAndHashCode.java │ ├── DefaultConstructorAspectAnnotationProcessor.java │ └── StateBasedEqualsAndHashCodeAspectAnnotationProcessor.java └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cstettler/ddd-to-the-code-workshop-sample/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wallet ( 2 | id VARCHAR PRIMARY KEY, 3 | data TEXT 4 | ); 5 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip -------------------------------------------------------------------------------- /docs/bounded-context-and-use-cases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cstettler/ddd-to-the-code-workshop-sample/HEAD/docs/bounded-context-and-use-cases.png -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/static/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 7rem; 3 | } 4 | 5 | h3 { 6 | margin-bottom: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/resources/static/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 7rem; 3 | } 4 | 5 | h3 { 6 | margin-bottom: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/static/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 7rem; 3 | } 4 | 5 | h3 { 6 | margin-bottom: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_registration ( 2 | id VARCHAR PRIMARY KEY, 3 | user_handle VARCHAR UNIQUE, 4 | data TEXT 5 | ); 6 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.activemq.broker-url=vm://localhost-${random.uuid}?broker.persistent=false&broker.useJmx=false&broker.useShutdownHook=false 2 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.activemq.broker-url=vm://localhost-${random.uuid}?broker.persistent=false&broker.useJmx=false&broker.useShutdownHook=false 2 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.activemq.broker-url=vm://localhost-${random.uuid}?broker.persistent=false&broker.useJmx=false&broker.useShutdownHook=false 2 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user ( 2 | id VARCHAR PRIMARY KEY, 3 | data TEXT 4 | ); 5 | 6 | CREATE TABLE booking ( 7 | id VARCHAR PRIMARY KEY, 8 | data TEXT 9 | ); 10 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE domain_event ( 2 | id VARCHAR NOT NULL, 3 | type VARCHAR NOT NULL, 4 | payload TEXT NOT NULL, 5 | published_at TIMESTAMP NOT NULL, 6 | dispatched bit DEFAULT 0 7 | ); 8 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8083 2 | spring.jms.pub-sub-domain=true 3 | spring.activemq.broker-url=vm://(broker://(tcp://localhost:61083,network:static:(tcp://localhost:61081,tcp://localhost:61082,tcp://localhost:61083))/rental?persistent=false&useJmx=false&useShutdownHook=false)/rental 4 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | spring.jms.pub-sub-domain=true 3 | spring.activemq.broker-url=vm://(broker://(tcp://localhost:61082,network:static:(tcp://localhost:61081,tcp://localhost:61082,tcp://localhost:61083))/accounting?persistent=false&useJmx=false&useShutdownHook=false)/accounting 4 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 2 | spring.jms.pub-sub-domain=true 3 | spring.activemq.broker-url=vm://(broker://(tcp://localhost:61081,network:static:(tcp://localhost:61081,tcp://localhost:61082,tcp://localhost:61083))/registration?persistent=false&useJmx=false&useShutdownHook=false)/registration 4 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/domain/DomainEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.InfrastructureService; 4 | 5 | @InfrastructureService 6 | public interface DomainEventPublisher { 7 | 8 | void publish(Object domainEvent); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/SmsNotificationSender.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.InfrastructureService; 4 | 5 | @InfrastructureService 6 | public interface SmsNotificationSender { 7 | 8 | void sendSmsTo(PhoneNumber phoneNumber, String text); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | 5 | @Repository 6 | public interface UserRepository { 7 | 8 | void add(User user) throws UserAlreadyExistsException; 9 | 10 | User get(UserId userId) throws UserNotExistingException; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/BikeRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | 5 | import java.util.Collection; 6 | 7 | @Repository 8 | public interface BikeRepository { 9 | 10 | void add(Bike bike); 11 | 12 | Bike get(NumberPlate numberPlate); 13 | 14 | Collection findAll(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/RepositoryImplementationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | import org.springframework.core.type.filter.AnnotationTypeFilter; 5 | 6 | public class RepositoryImplementationFilter extends AnnotationTypeFilter { 7 | 8 | public RepositoryImplementationFilter() { 9 | super(Repository.class, false, true); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | 8 | /** 9 | * Represents a business exception. Business exceptions signal attempts to invalidly change business invariants. 10 | */ 11 | @Target(TYPE) 12 | @Documented 13 | public @interface BusinessException { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /etc/architecture-governance/index.adoc: -------------------------------------------------------------------------------- 1 | = Architecture Governance for Bike Sharing System 2 | 3 | This document describes the architecture constraints for the implementation of the bike sharing system according to the 4 | concepts of the onion architecture. It contains the various concepts and constraints to be applied to and checked 5 | against the code base using jQAssistant. 6 | 7 | include::concepts.adoc[] 8 | 9 | include::constraints.adoc[] 10 | 11 | include::reports.adoc[] 12 | 13 | == Summary 14 | 15 | include::jQA:Summary[] 16 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/InfrastructureServiceImplementationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | import com.github.cstettler.dddttc.stereotype.InfrastructureService; 4 | import org.springframework.core.type.filter.AnnotationTypeFilter; 5 | 6 | public class InfrastructureServiceImplementationFilter extends AnnotationTypeFilter { 7 | 8 | public InfrastructureServiceImplementationFilter() { 9 | super(InfrastructureService.class, false, true); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookingRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | 5 | import java.util.Collection; 6 | 7 | @Repository 8 | public interface BookingRepository { 9 | 10 | void add(Booking booking); 11 | 12 | void update(Booking booking); 13 | 14 | Booking get(BookingId bookingId) throws BookingNotExistingException; 15 | 16 | Collection findAll(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/UserAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | public class UserAlreadyExistsException extends RuntimeException { 4 | 5 | private UserAlreadyExistsException(UserId userId) { 6 | super("user '" + userId.value() + "' already exists"); 7 | } 8 | 9 | public static UserAlreadyExistsException userAlreadyExists(UserId userId) { 10 | return new UserAlreadyExistsException(userId); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/ValueObject.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | 8 | /** 9 | * Represents a value object. Value objects are immutable. Equality between two value object instances is defined by 10 | * their type and their internal state. 11 | */ 12 | @Target(TYPE) 13 | @Documented 14 | public @interface ValueObject { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/AggregateId.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.METHOD; 7 | 8 | /** 9 | * Represents the aggregate id of an aggregate instance. Each aggregate contains one single annotated accessor method 10 | * providing access to the aggregate id. 11 | */ 12 | @Target(METHOD) 13 | @Documented 14 | public @interface AggregateId { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/UserId.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class UserId { 7 | 8 | private String value; 9 | 10 | private UserId(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static UserId userId(String value) { 19 | return new UserId(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/LastName.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class LastName { 7 | 8 | private final String value; 9 | 10 | private LastName(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static LastName lastName(String value) { 19 | return new LastName(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/FirstName.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class FirstName { 7 | 8 | private final String value; 9 | 10 | private FirstName(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static FirstName firstName(String value) { 19 | return new FirstName(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/WalletRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | 5 | import java.util.Collection; 6 | 7 | @Repository 8 | public interface WalletRepository { 9 | 10 | void add(Wallet wallet) throws WalletAlreadyExistsException; 11 | 12 | void update(Wallet wallet) throws WalletNotExistingException; 13 | 14 | Wallet get(WalletOwner walletOwner) throws WalletNotExistingException; 15 | 16 | Collection findAll(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/NumberPlate.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class NumberPlate { 7 | 8 | private String value; 9 | 10 | private NumberPlate(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static NumberPlate numberPlate(String value) { 19 | return new NumberPlate(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/WalletOwner.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class WalletOwner { 7 | 8 | private final String value; 9 | 10 | private WalletOwner(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static WalletOwner walletOwner(String value) { 19 | return new WalletOwner(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserHandle.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class UserHandle { 7 | 8 | private final String value; 9 | 10 | private UserHandle(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | public static UserHandle userHandle(String value) { 19 | return new UserHandle(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/support/Validations.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain.support; 2 | 3 | import static com.github.cstettler.dddttc.registration.domain.support.MandatoryParameterMissingException.mandatoryPropertyMissing; 4 | 5 | public class Validations { 6 | 7 | private Validations() { 8 | } 9 | 10 | public static void validateNotNull(String parameterName, Object parameterValue) { 11 | if (parameterValue == null) { 12 | throw mandatoryPropertyMissing(parameterName); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/UserNotExistingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class UserNotExistingException extends RuntimeException { 7 | 8 | private UserNotExistingException(UserId userid) { 9 | super("user '" + userid.value() + "' does not exist"); 10 | } 11 | 12 | public static UserNotExistingException userNotExisting(UserId userid) { 13 | return new UserNotExistingException(userid); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/infrastructure/event/TestDomainEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | 5 | @DomainEvent 6 | class TestDomainEvent { 7 | 8 | private String value; 9 | 10 | private TestDomainEvent(String value) { 11 | this.value = value; 12 | } 13 | 14 | String value() { 15 | return this.value; 16 | } 17 | 18 | static TestDomainEvent testDomainEvent(String value) { 19 | return new TestDomainEvent(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/TransactionReference.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | class TransactionReference { 7 | 8 | private final String value; 9 | 10 | private TransactionReference(String value) { 11 | this.value = value; 12 | } 13 | 14 | String value() { 15 | return this.value; 16 | } 17 | 18 | static TransactionReference transactionReference(String value) { 19 | return new TransactionReference(value); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/BikeNotExistingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class BikeNotExistingException extends RuntimeException { 7 | 8 | private BikeNotExistingException(NumberPlate numberPlate) { 9 | super("bike '" + numberPlate.value() + "' does not exist"); 10 | } 11 | 12 | public static BikeNotExistingException bikeNotExisting(NumberPlate numberPlate) { 13 | return new BikeNotExistingException(numberPlate); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.github.cstettler.dddttc 8 | ddd-to-the-code-workshop 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | ddd-to-the-code-workshop-stereotype 13 | jar 14 | 15 | 16 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/WalletNotExistingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class WalletNotExistingException extends RuntimeException { 7 | 8 | private WalletNotExistingException(WalletOwner walletOwner) { 9 | super("wallet '" + walletOwner.value() + "' does not exist"); 10 | } 11 | 12 | public static WalletNotExistingException walletNotExisting(WalletOwner walletOwner) { 13 | return new WalletNotExistingException(walletOwner); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookingNotExistingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class BookingNotExistingException extends RuntimeException { 7 | 8 | private BookingNotExistingException(BookingId bookingId) { 9 | super("booking '" + bookingId.value() + "' does not exist"); 10 | } 11 | 12 | public static BookingNotExistingException bookingNotExisting(BookingId bookingId) { 13 | return new BookingNotExistingException(bookingId); 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /etc/architecture-governance/reports.adoc: -------------------------------------------------------------------------------- 1 | == Reports 2 | 3 | [[dddttc:StereotypesUsageReport.graphml]] 4 | .Report showing all dependencies between stereotypes (GraphML) 5 | [source,cypher,role=concept,requiresConcepts="dddttc:StereotypeConcept"] 6 | ---- 7 | MATCH (m:Stereotype)-[d:DEPENDS_ON]->(n:Stereotype) 8 | RETURN m, d, n 9 | ---- 10 | 11 | 12 | [[dddttc:StereotypesUsageReport]] 13 | .Report showing all dependencies between stereotypes (PlantUML) 14 | [source,cypher,role=concept,requiresConcepts="dddttc:*Concept",reportType="plantuml-component-diagram"] 15 | ---- 16 | MATCH (m:Stereotype)-[d:DEPENDS_ON]->(n:Stereotype) 17 | RETURN m, d, n 18 | ---- 19 | 20 | (more reports to follow) -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/WalletAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class WalletAlreadyExistsException extends RuntimeException { 7 | 8 | private WalletAlreadyExistsException(WalletOwner walletOwner) { 9 | super("wallet '" + walletOwner.value() + "' already exists"); 10 | } 11 | 12 | public static WalletAlreadyExistsException walletAlreadyExists(WalletOwner walletOwner) { 13 | return new WalletAlreadyExistsException(walletOwner); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberNotSwissException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class PhoneNumberNotSwissException extends RuntimeException { 7 | 8 | private PhoneNumberNotSwissException(PhoneNumber phoneNumber) { 9 | super("phone number '" + phoneNumber.value() + "' is not swiss"); 10 | } 11 | 12 | static PhoneNumberNotSwissException phoneNumberNotSwiss(PhoneNumber phoneNumber) { 13 | return new PhoneNumberNotSwissException(phoneNumber); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Repository; 4 | 5 | @Repository 6 | public interface UserRegistrationRepository { 7 | 8 | void add(UserRegistration userRegistration) throws UserHandleAlreadyInUseException; 9 | 10 | void update(UserRegistration userRegistration) throws UserRegistrationNotExistingException; 11 | 12 | UserRegistration get(UserRegistrationId userRegistrationId) throws UserRegistrationNotExistingException; 13 | 14 | UserRegistration find(UserHandle userHandle); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/DomainService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents a domain service implementing business logic not directly assignable to a specific aggregate. Domain 12 | * services may also provide domain event handlers. 13 | */ 14 | @Target(TYPE) 15 | @Retention(RUNTIME) 16 | @Documented 17 | public @interface DomainService { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserHandleAlreadyInUseException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class UserHandleAlreadyInUseException extends RuntimeException { 7 | 8 | private UserHandleAlreadyInUseException(UserHandle userHandle) { 9 | super("user handle '" + userHandle.value() + "' is already in use"); 10 | } 11 | 12 | public static UserHandleAlreadyInUseException userHandleAlreadyInUse(UserHandle userHandle) { 13 | return new UserHandleAlreadyInUseException(userHandle); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookingAlreadyCompletedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class BookingAlreadyCompletedException extends RuntimeException { 7 | 8 | private BookingAlreadyCompletedException(BookingId bookingId) { 9 | super("booking '" + bookingId.value() + "' has already been completed"); 10 | } 11 | 12 | public static BookingAlreadyCompletedException bookingAlreadyCompleted(BookingId bookingId) { 13 | return new BookingAlreadyCompletedException(bookingId); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/Aggregate.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | 8 | /** 9 | * Represents an aggregate with core domain logic and related state. An aggregate ensures its domain invariants and 10 | * represents the minimal scope of a business transaction. Aggregate instances are identified by their aggregate id. 11 | * Equality between two aggregates instances is defined by their type and their aggregate id. 12 | */ 13 | @Target(TYPE) 14 | @Documented 15 | public @interface Aggregate { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/WalletInitializedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | 5 | @DomainEvent 6 | class WalletInitializedEvent { 7 | 8 | private final WalletOwner walletOwner; 9 | 10 | private WalletInitializedEvent(WalletOwner walletOwner) { 11 | this.walletOwner = walletOwner; 12 | } 13 | 14 | WalletOwner walletOwner() { 15 | return this.walletOwner; 16 | } 17 | 18 | static WalletInitializedEvent walletInitialized(WalletOwner walletOwner) { 19 | return new WalletInitializedEvent(walletOwner); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/ApplicationService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents an application service responsible for providing access to the domain to external clients. An application 12 | * service orchestrates use cases, but does not contain business logic. 13 | */ 14 | @Target(TYPE) 15 | @Retention(RUNTIME) 16 | @Documented 17 | public @interface ApplicationService { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberNotYetVerifiedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class PhoneNumberNotYetVerifiedException extends RuntimeException { 7 | 8 | private PhoneNumberNotYetVerifiedException(PhoneNumber phoneNumber) { 9 | super("phone number '" + phoneNumber.value() + "' has not yet been verified"); 10 | } 11 | 12 | static PhoneNumberNotYetVerifiedException phoneNumberNotYetVerified(PhoneNumber phoneNumber) { 13 | return new PhoneNumberNotYetVerifiedException(phoneNumber); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/support/MandatoryParameterMissingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain.support; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class MandatoryParameterMissingException extends RuntimeException { 7 | 8 | private MandatoryParameterMissingException(String parameterName) { 9 | super("mandatory parameter '" + parameterName + "' is missing"); 10 | } 11 | 12 | static MandatoryParameterMissingException mandatoryPropertyMissing(String parameterName) { 13 | return new MandatoryParameterMissingException(parameterName); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberAlreadyVerifiedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class PhoneNumberAlreadyVerifiedException extends RuntimeException { 7 | 8 | private PhoneNumberAlreadyVerifiedException(PhoneNumber phoneNumber) { 9 | super("phone number '" + phoneNumber.value() + "' has already been verified"); 10 | } 11 | 12 | static PhoneNumberAlreadyVerifiedException phoneNumberAlreadyVerified(PhoneNumber phoneNumber) { 13 | return new PhoneNumberAlreadyVerifiedException(phoneNumber); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/BookingAlreadyBilledException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class BookingAlreadyBilledException extends RuntimeException { 7 | 8 | private BookingAlreadyBilledException(Wallet wallet, Booking booking) { 9 | super("booking '" + booking.id().value() + " has already been billed to wallet '" + wallet.walletOwner().value() + "'"); 10 | } 11 | 12 | static BookingAlreadyBilledException bookingAlreadyBilled(Wallet wallet, Booking booking) { 13 | return new BookingAlreadyBilledException(wallet, booking); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumber.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class PhoneNumber { 7 | 8 | private final String value; 9 | 10 | private PhoneNumber(String value) { 11 | this.value = value; 12 | } 13 | 14 | public String value() { 15 | return this.value; 16 | } 17 | 18 | boolean isSwiss() { 19 | return this.value != null && (this.value.startsWith("+41") || this.value.startsWith("0041")); 20 | } 21 | 22 | public static PhoneNumber phoneNumber(String value) { 23 | return new PhoneNumber(value); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/FullName.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | public class FullName { 7 | 8 | private final String firstName; 9 | private final String lastName; 10 | 11 | private FullName(String firstName, String lastName) { 12 | this.firstName = firstName; 13 | this.lastName = lastName; 14 | } 15 | 16 | public String firstAndLastName() { 17 | return this.firstName + " " + this.lastName; 18 | } 19 | 20 | public static FullName fullName(String firstName, String lastName) { 21 | return new FullName(firstName, lastName); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/test/java/com/github/cstettler/dddttc/rental/RentalApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental; 2 | 3 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | @ExtendWith(SpringExtension.class) 11 | @SpringBootTest 12 | @EnableComponentScanExclusions 13 | @ActiveProfiles("test") 14 | class RentalApplicationTests { 15 | 16 | @Test 17 | void bootstrappingApplicationContext_works() { 18 | // do nothing 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationNotExistingException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class UserRegistrationNotExistingException extends RuntimeException { 7 | 8 | private UserRegistrationNotExistingException(UserRegistrationId userRegistrationId) { 9 | super("user registration '" + userRegistrationId.value() + "' does not exist"); 10 | } 11 | 12 | public static UserRegistrationNotExistingException userRegistrationNotExisting(UserRegistrationId userRegistrationId) { 13 | return new UserRegistrationNotExistingException(userRegistrationId); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/infrastructure/sms/LoggingSmsNotificationSender.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.infrastructure.sms; 2 | 3 | import com.github.cstettler.dddttc.registration.domain.PhoneNumber; 4 | import com.github.cstettler.dddttc.registration.domain.SmsNotificationSender; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | class LoggingSmsNotificationSender implements SmsNotificationSender { 9 | 10 | private static Logger LOGGER = LoggerFactory.getLogger(LoggingSmsNotificationSender.class); 11 | 12 | @Override 13 | public void sendSmsTo(PhoneNumber phoneNumber, String text) { 14 | LOGGER.info("SMS to '" + phoneNumber.value() + "': '" + text + "'"); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/java/com/github/cstettler/dddttc/accounting/AccountingApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting; 2 | 3 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | @ExtendWith(SpringExtension.class) 11 | @SpringBootTest 12 | @EnableComponentScanExclusions 13 | @ActiveProfiles("test") 14 | class AccountingApplicationTests { 15 | 16 | @Test 17 | void bootstrappingApplicationContext_works() { 18 | // do nothing 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/BikeAlreadyBookedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 4 | import com.github.cstettler.dddttc.stereotype.BusinessException; 5 | 6 | @BusinessException 7 | public class BikeAlreadyBookedException extends RuntimeException { 8 | 9 | private BikeAlreadyBookedException(NumberPlate numberPlate, UserId userId) { 10 | super("bike '" + numberPlate.value() + "' is already booked by user '" + userId.value() + "'"); 11 | } 12 | 13 | static BikeAlreadyBookedException bikeAlreadyBooked(NumberPlate numberPlate, UserId userId) { 14 | return new BikeAlreadyBookedException(numberPlate, userId); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/BookingId.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | import static java.time.ZoneOffset.UTC; 8 | 9 | @ValueObject 10 | class BookingId { 11 | 12 | private final String value; 13 | 14 | private BookingId(WalletOwner walletOwner, LocalDateTime startedAt) { 15 | this.value = walletOwner.value() + "-" + startedAt.toEpochSecond(UTC); 16 | } 17 | 18 | String value() { 19 | return this.value; 20 | } 21 | 22 | static BookingId bookingId(WalletOwner walletOwner, LocalDateTime startedAt) { 23 | return new BookingId(walletOwner, startedAt); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/RegistrationApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration; 2 | 3 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.test.context.junit.jupiter.SpringExtension; 9 | 10 | @ExtendWith(SpringExtension.class) 11 | @SpringBootTest 12 | @EnableComponentScanExclusions 13 | @ActiveProfiles("test") 14 | class RegistrationApplicationTests { 15 | 16 | @Test 17 | void bootstrappingApplicationContext_works() { 18 | // do nothing 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberVerificationCodeInvalidException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class PhoneNumberVerificationCodeInvalidException extends RuntimeException { 7 | 8 | private PhoneNumberVerificationCodeInvalidException(VerificationCode verificationCode) { 9 | super("phone number verification code '" + verificationCode.value() + "' is invalid"); 10 | } 11 | 12 | static PhoneNumberVerificationCodeInvalidException phoneNumberVerificationCodeInvalid(VerificationCode verificationCode) { 13 | return new PhoneNumberVerificationCodeInvalidException(verificationCode); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationAlreadyCompletedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.BusinessException; 4 | 5 | @BusinessException 6 | public class UserRegistrationAlreadyCompletedException extends RuntimeException { 7 | 8 | private UserRegistrationAlreadyCompletedException(UserRegistrationId userRegistrationId) { 9 | super("user registration '" + userRegistrationId.value() + "' has already been completed"); 10 | } 11 | 12 | static UserRegistrationAlreadyCompletedException userRegistrationAlreadyCompleted(UserRegistrationId userRegistrationId) { 13 | return new UserRegistrationAlreadyCompletedException(userRegistrationId); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookingId.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import static java.util.UUID.randomUUID; 6 | 7 | @ValueObject 8 | public class BookingId { 9 | 10 | private String value; 11 | 12 | private BookingId() { 13 | this(randomUUID().toString()); 14 | } 15 | 16 | private BookingId(String value) { 17 | this.value = value; 18 | } 19 | 20 | public String value() { 21 | return this.value; 22 | } 23 | 24 | public static BookingId bookingId(String value) { 25 | return new BookingId(value); 26 | } 27 | 28 | static BookingId newBookingId() { 29 | return new BookingId(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/Repository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents a repository for aggregates. A repository is responsible for accepting aggregates of a specific type, 12 | * keeping them and returning the as requested by the domain. The repository interface forms part of the domain, the 13 | * implementation (e.g. against a database) is part of the infrastructure. 14 | */ 15 | @Target(TYPE) 16 | @Retention(RUNTIME) 17 | @Documented 18 | public @interface Repository { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/UserHandleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.cstettler.dddttc.registration.domain.UserHandle.userHandle; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.is; 8 | 9 | class UserHandleTests { 10 | 11 | @Test 12 | void equals_userHandlesWithSameValues_returnsTrue() { 13 | // arrange 14 | UserHandle userHandleOne = userHandle("peter"); 15 | UserHandle userHandleTwo = userHandle("peter"); 16 | 17 | // act + assert 18 | assertThat(userHandleOne.equals(userHandleTwo), is(true)); 19 | assertThat(userHandleTwo.equals(userHandleOne), is(true)); 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/InfrastructureService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents an infrastructure service. An infrastructure service provides functionality to the domain that requires 12 | * additional infrastructure only available outside of the domain. The infrastructure service interface forms part of 13 | * the domain, the implementation is part of the infrastructure. 14 | */ 15 | @Target(TYPE) 16 | @Retention(RUNTIME) 17 | @Documented 18 | public @interface InfrastructureService { 19 | } 20 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rental 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Error

18 | 19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | @ValueObject 6 | class Transaction { 7 | 8 | private final TransactionReference reference; 9 | private final Amount amount; 10 | 11 | private Transaction(TransactionReference reference, Amount amount) { 12 | this.reference = reference; 13 | this.amount = amount; 14 | } 15 | 16 | TransactionReference reference() { 17 | return this.reference; 18 | } 19 | 20 | Amount amount() { 21 | return this.amount; 22 | } 23 | 24 | static Transaction transaction(TransactionReference transactionReference, Amount amount) { 25 | return new Transaction(transactionReference, amount); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/TransactionWithSameReferenceAlreadyAppliedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | class TransactionWithSameReferenceAlreadyAppliedException extends RuntimeException { 4 | 5 | private TransactionWithSameReferenceAlreadyAppliedException(WalletOwner walletOwner, TransactionReference transactionReference) { 6 | super("transaction with reference '" + transactionReference.value() + " has already been applied to wallet '" + walletOwner.value() + "'"); 7 | } 8 | 9 | static TransactionWithSameReferenceAlreadyAppliedException transactionWithSameReferenceAlreadyApplied(WalletOwner walletOwner, TransactionReference transactionReference) { 10 | return new TransactionWithSameReferenceAlreadyAppliedException(walletOwner, transactionReference); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Registration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Error

18 | 19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/user/User.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.user; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Aggregate; 4 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 5 | 6 | @Aggregate 7 | public class User { 8 | 9 | private final UserId id; 10 | private final FirstName firstName; 11 | private final LastName lastName; 12 | 13 | private User(UserId id, FirstName firstName, LastName lastName) { 14 | this.id = id; 15 | this.firstName = firstName; 16 | this.lastName = lastName; 17 | } 18 | 19 | public UserId id() { 20 | return this.id; 21 | } 22 | 23 | @AggregateFactory(User.class) 24 | public static User newUser(UserId userId, FirstName firstName, LastName lastName) { 25 | return new User(userId, firstName, lastName); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationId.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import static java.util.UUID.randomUUID; 6 | 7 | @ValueObject 8 | public class UserRegistrationId { 9 | 10 | private final String value; 11 | 12 | private UserRegistrationId() { 13 | this(randomUUID().toString()); 14 | } 15 | 16 | private UserRegistrationId(String value) { 17 | this.value = value; 18 | } 19 | 20 | public String value() { 21 | return this.value; 22 | } 23 | 24 | public static UserRegistrationId userRegistrationId(String value) { 25 | return new UserRegistrationId(value); 26 | } 27 | 28 | static UserRegistrationId newUserRegistrationId() { 29 | return new UserRegistrationId(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/Amount.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import static java.math.BigDecimal.ZERO; 8 | 9 | @ValueObject 10 | public class Amount { 11 | 12 | private final BigDecimal value; 13 | 14 | private Amount(BigDecimal value) { 15 | this.value = value; 16 | } 17 | 18 | public BigDecimal value() { 19 | return this.value; 20 | } 21 | 22 | Amount negate() { 23 | return amount(this.value.negate()); 24 | } 25 | 26 | Amount add(Amount other) { 27 | return amount(this.value.add(other.value)); 28 | } 29 | 30 | static Amount zero() { 31 | return amount(ZERO); 32 | } 33 | 34 | static Amount amount(BigDecimal value) { 35 | return new Amount(value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/DomainEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents a handler of a domain event. A domain event handler consumes domain events of a specific type. Domain 12 | * event handlers are responsible for dealing with receiving the same domain event multiple times (at-least-once 13 | * semantics). 14 | *

15 | * Domain event handlers are typically part of domain services. A domain event handler method must accept a single 16 | * parameter of the domain event type handled. 17 | */ 18 | @Target(METHOD) 19 | @Retention(RUNTIME) 20 | @Documented 21 | public @interface DomainEventHandler { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/PendingDomainEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | class PendingDomainEvent { 6 | 7 | private final String id; 8 | private final String type; 9 | private final String payload; 10 | private final LocalDateTime publishedAt; 11 | 12 | PendingDomainEvent(String id, String type, String payload, LocalDateTime publishedAt) { 13 | this.id = id; 14 | this.type = type; 15 | this.payload = payload; 16 | this.publishedAt = publishedAt; 17 | } 18 | 19 | String id() { 20 | return this.id; 21 | } 22 | 23 | String type() { 24 | return this.type; 25 | } 26 | 27 | String payload() { 28 | return this.payload; 29 | } 30 | 31 | LocalDateTime publishedAt() { 32 | return this.publishedAt; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/java/com/github/cstettler/dddttc/accounting/domain/WalletBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.support.ReflectionBasedStateBuilder; 4 | 5 | @SuppressWarnings("UnusedReturnValue") 6 | public class WalletBuilder extends ReflectionBasedStateBuilder { 7 | 8 | private WalletBuilder() { 9 | walletOwner(anyWalletOwner()); 10 | } 11 | 12 | public WalletBuilder walletOwner(String idValue) { 13 | return walletOwner(WalletOwner.walletOwner(idValue)); 14 | } 15 | 16 | public WalletBuilder walletOwner(WalletOwner walletOwner) { 17 | return recordProperty(this, "walletOwner", walletOwner); 18 | } 19 | 20 | public static WalletBuilder wallet() { 21 | return new WalletBuilder(); 22 | } 23 | 24 | private static WalletOwner anyWalletOwner() { 25 | return WalletOwner.walletOwner(randomString(10)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventTypeResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import java.util.List; 4 | 5 | class DomainEventTypeResolver { 6 | 7 | private final List allDomainEventTypeMappings; 8 | 9 | DomainEventTypeResolver(List allDomainEventTypeMappings) { 10 | this.allDomainEventTypeMappings = allDomainEventTypeMappings; 11 | } 12 | 13 | String resolveDomainEventType(Class domainEventClass) { 14 | for (DomainEventTypeMappings domainEventTypeMappings : this.allDomainEventTypeMappings) { 15 | String domainEventType = domainEventTypeMappings.getDomainEventTypeIfAvailable(domainEventClass); 16 | 17 | if (domainEventType != null) { 18 | return domainEventType; 19 | } 20 | } 21 | 22 | return domainEventClass.getName(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/infrastructure/event/TestDomainService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEventHandler; 4 | import com.github.cstettler.dddttc.stereotype.DomainService; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @DomainService 10 | class TestDomainService { 11 | 12 | private List recordedDomainEvents; 13 | 14 | TestDomainService() { 15 | this.recordedDomainEvents = new ArrayList<>(); 16 | } 17 | 18 | @DomainEventHandler 19 | void handleTestDomainEventWithImplicitType(TestDomainEvent event) { 20 | this.recordedDomainEvents.add(event); 21 | } 22 | 23 | List recordedDomainEvents() { 24 | return this.recordedDomainEvents; 25 | } 26 | 27 | void clearRecordedDomainEvents() { 28 | this.recordedDomainEvents.clear(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/ReleaseBikeService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.booking.BookingCompletedEvent; 4 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 5 | import com.github.cstettler.dddttc.stereotype.DomainEventHandler; 6 | import com.github.cstettler.dddttc.stereotype.DomainService; 7 | 8 | @DomainService 9 | class ReleaseBikeService { 10 | 11 | private final BikeRepository bikeRepository; 12 | 13 | ReleaseBikeService(BikeRepository bikeRepository) { 14 | this.bikeRepository = bikeRepository; 15 | } 16 | 17 | @DomainEventHandler 18 | void releaseBike(BookingCompletedEvent event) { 19 | NumberPlate numberPlate = event.numberPlate(); 20 | UserId lastUserId = event.userId(); 21 | 22 | Bike bike = this.bikeRepository.get(numberPlate); 23 | bike.markAsReturnedBy(lastUserId); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/BookingFeePolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainService; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import static com.github.cstettler.dddttc.accounting.domain.Amount.amount; 8 | import static java.lang.Math.round; 9 | import static java.math.BigDecimal.valueOf; 10 | 11 | @DomainService 12 | public class BookingFeePolicy { 13 | 14 | private final BigDecimal initialPrice; 15 | private final BigDecimal pricePerMinute; 16 | 17 | BookingFeePolicy() { 18 | this.initialPrice = new BigDecimal("1.50"); 19 | this.pricePerMinute = new BigDecimal("0.25"); 20 | } 21 | 22 | Amount feeForBooking(Booking booking) { 23 | int roundedMinutes = round((float) booking.durationInSeconds() / 60); 24 | 25 | return amount(this.initialPrice.add(this.pricePerMinute.multiply(valueOf(roundedMinutes)))); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/AggregateFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.ElementType.TYPE; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | /** 12 | * Represents a factory responsible for creating new aggregate instances. An aggregate factory encapsulates the 13 | * knowledge required to create a new aggregate instance. An aggregate factory can either be a static method on an 14 | * aggregate, or a separate class, depending on the complexity of the instantiation process and the dependencies needed. 15 | */ 16 | @Target({TYPE, METHOD}) 17 | @Retention(RUNTIME) 18 | @Documented 19 | public @interface AggregateFactory { 20 | 21 | /** 22 | * The type of aggregate created 23 | */ 24 | Class value(); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/SendVerificationCodeSmsService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEventHandler; 4 | import com.github.cstettler.dddttc.stereotype.DomainService; 5 | 6 | @DomainService 7 | class SendVerificationCodeSmsService { 8 | 9 | private final SmsNotificationSender smsNotificationSender; 10 | 11 | SendVerificationCodeSmsService(SmsNotificationSender smsNotificationSender) { 12 | this.smsNotificationSender = smsNotificationSender; 13 | } 14 | 15 | @DomainEventHandler 16 | void sendVerificationCodeSmsToPhoneNumber(PhoneNumberVerificationCodeGeneratedEvent event) { 17 | PhoneNumber phoneNumber = event.phoneNumber(); 18 | VerificationCode verificationCode = event.verificationCode(); 19 | 20 | String smsText = "Your verification code is " + verificationCode.value(); 21 | 22 | this.smsNotificationSender.sendSmsTo(phoneNumber, smsText); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/application/BikeService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.application; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.Bike; 4 | import com.github.cstettler.dddttc.rental.domain.bike.BikeNotExistingException; 5 | import com.github.cstettler.dddttc.rental.domain.bike.BikeRepository; 6 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 7 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 8 | 9 | import java.util.Collection; 10 | 11 | @ApplicationService 12 | public class BikeService { 13 | 14 | private final BikeRepository bikeRepository; 15 | 16 | BikeService(BikeRepository bikeRepository) { 17 | this.bikeRepository = bikeRepository; 18 | } 19 | 20 | public Collection listBikes() { 21 | return this.bikeRepository.findAll(); 22 | } 23 | 24 | public Bike getBike(NumberPlate numberPlate) throws BikeNotExistingException { 25 | return this.bikeRepository.get(numberPlate); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/java/com/github/cstettler/dddttc/accounting/domain/WalletMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.support.ReflectionBasedStateMatcher; 4 | import org.hamcrest.Matcher; 5 | 6 | import java.util.Map; 7 | 8 | public class WalletMatcher extends ReflectionBasedStateMatcher { 9 | 10 | private WalletMatcher(Map> matchersByPropertyName) { 11 | super("wallet", matchersByPropertyName); 12 | } 13 | 14 | public static Builder walletWith() { 15 | return new Builder(); 16 | } 17 | 18 | 19 | public static class Builder extends MatcherBuilder { 20 | 21 | public Builder walletOwner(Matcher idMatcher) { 22 | return recordPropertyMatcher(this, "walletOwner", idMatcher); 23 | } 24 | 25 | @Override 26 | protected WalletMatcher build(Map> matchersByPropertyName) { 27 | return new WalletMatcher(matchersByPropertyName); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BikeUsage.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | import static java.time.ZoneOffset.UTC; 8 | 9 | @ValueObject 10 | public class BikeUsage { 11 | 12 | private final LocalDateTime startedAt; 13 | private final LocalDateTime endedAt; 14 | private final long durationInSeconds; 15 | 16 | private BikeUsage(LocalDateTime startedAt, LocalDateTime endedAt) { 17 | this.startedAt = startedAt; 18 | this.endedAt = endedAt; 19 | this.durationInSeconds = usageTimeInSeconds(startedAt, endedAt); 20 | } 21 | 22 | private long usageTimeInSeconds(LocalDateTime startedAt, LocalDateTime endedAt) { 23 | return endedAt.toEpochSecond(UTC) - startedAt.toEpochSecond(UTC); 24 | } 25 | 26 | static BikeUsage bikeUsage(LocalDateTime startedAt, LocalDateTime endedAt) { 27 | return new BikeUsage(startedAt, endedAt); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/test/ScenarioTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.test; 2 | 3 | 4 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.ActiveProfiles; 9 | import org.springframework.test.context.junit.jupiter.SpringExtension; 10 | 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.Target; 13 | 14 | import static java.lang.annotation.ElementType.TYPE; 15 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 16 | 17 | @Target(TYPE) 18 | @Retention(RUNTIME) 19 | @ExtendWith(SpringExtension.class) 20 | @ExtendWith(DatabaseCleaner.class) 21 | @SpringBootTest 22 | @WithDomainEventTestSupport 23 | @AutoConfigureTestDatabase 24 | @EnableComponentScanExclusions 25 | @ActiveProfiles("test") 26 | public @interface ScenarioTest { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberVerificationCodeGeneratedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | 5 | @DomainEvent 6 | public class PhoneNumberVerificationCodeGeneratedEvent { 7 | 8 | private final PhoneNumber phoneNumber; 9 | private final VerificationCode verificationCode; 10 | 11 | private PhoneNumberVerificationCodeGeneratedEvent(PhoneNumber phoneNumber, VerificationCode verificationCode) { 12 | this.phoneNumber = phoneNumber; 13 | this.verificationCode = verificationCode; 14 | } 15 | 16 | public PhoneNumber phoneNumber() { 17 | return this.phoneNumber; 18 | } 19 | 20 | public VerificationCode verificationCode() { 21 | return this.verificationCode; 22 | } 23 | 24 | static PhoneNumberVerificationCodeGeneratedEvent phoneNumberVerificationCodeGenerated(PhoneNumber phoneNumber, VerificationCode verificationCode) { 25 | return new PhoneNumberVerificationCodeGeneratedEvent(phoneNumber, verificationCode); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-stereotype/src/main/java/com/github/cstettler/dddttc/stereotype/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.stereotype; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Represents an event relevant to the business domain. A domain event describes a fact that has happened in the past. 12 | * Domain events are immutable and carry the state relevant for consumers to understand the semantics of the domain 13 | * event. 14 | */ 15 | @Target(TYPE) 16 | @Retention(RUNTIME) 17 | @Documented 18 | public @interface DomainEvent { 19 | 20 | /** 21 | * Type of the domain event. Must be defined for cross-bounded context domain events, but are is not mandatory for 22 | * intra-bounded context domain events. If defined, the type must be unique within the whole system. If not defined, 23 | * the type of the domain event is derived from the domain event class. 24 | */ 25 | String value() default ""; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/application/UserService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.application; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.user.FirstName; 4 | import com.github.cstettler.dddttc.rental.domain.user.LastName; 5 | import com.github.cstettler.dddttc.rental.domain.user.User; 6 | import com.github.cstettler.dddttc.rental.domain.user.UserAlreadyExistsException; 7 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 8 | import com.github.cstettler.dddttc.rental.domain.user.UserRepository; 9 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 10 | 11 | import static com.github.cstettler.dddttc.rental.domain.user.User.newUser; 12 | 13 | @ApplicationService 14 | public class UserService { 15 | 16 | private final UserRepository userRepository; 17 | 18 | UserService(UserRepository userRepository) { 19 | this.userRepository = userRepository; 20 | } 21 | 22 | public void addUser(UserId userId, FirstName firstName, LastName lastName) throws UserAlreadyExistsException { 23 | User user = newUser(userId, firstName, lastName); 24 | 25 | this.userRepository.add(user); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/InitializeBikesService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainService; 4 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 5 | 6 | import static com.github.cstettler.dddttc.rental.domain.bike.Bike.newBike; 7 | import static com.github.cstettler.dddttc.rental.domain.bike.NumberPlate.numberPlate; 8 | 9 | @DomainService 10 | public class InitializeBikesService { 11 | 12 | private final BikeRepository bikeRepository; 13 | private final DomainEventPublisher domainEventPublisher; 14 | 15 | InitializeBikesService(BikeRepository bikeRepository, DomainEventPublisher domainEventPublisher) { 16 | this.bikeRepository = bikeRepository; 17 | this.domainEventPublisher = domainEventPublisher; 18 | } 19 | 20 | public void initializeBikes() { 21 | this.bikeRepository.add(newBike(numberPlate("ZH-123"), this.domainEventPublisher)); 22 | this.bikeRepository.add(newBike(numberPlate("ZH-987"), this.domainEventPublisher)); 23 | this.bikeRepository.add(newBike(numberPlate("ZH-666"), this.domainEventPublisher)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/infrastructure/bootstrap/BikeInitializationTrigger.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.infrastructure.bootstrap; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.InitializeBikesService; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.transaction.PlatformTransactionManager; 6 | import org.springframework.transaction.support.TransactionTemplate; 7 | 8 | import javax.annotation.PostConstruct; 9 | 10 | @Component 11 | class BikeInitializationTrigger { 12 | 13 | private final InitializeBikesService initializeBikesService; 14 | private final TransactionTemplate transactionTemplate; 15 | 16 | BikeInitializationTrigger(InitializeBikesService initializeBikesService, PlatformTransactionManager transactionManager) { 17 | this.initializeBikesService = initializeBikesService; 18 | this.transactionTemplate = new TransactionTemplate(transactionManager); 19 | } 20 | 21 | @PostConstruct 22 | void triggerBikeInitialization() { 23 | this.transactionTemplate.execute((status) -> { 24 | this.initializeBikesService.initializeBikes(); 25 | 26 | return null; 27 | }); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-aspect/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.github.cstettler.dddttc 8 | ddd-to-the-code-workshop 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | ddd-to-the-code-workshop-aspect 13 | jar 14 | 15 | 16 | 17 | com.github.cstettler.dddttc 18 | ddd-to-the-code-workshop-stereotype 19 | ${project.version} 20 | compile 21 | 22 | 23 | 24 | com.google.auto.service 25 | auto-service 26 | 1.0-rc3 27 | compile 28 | true 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/Booking.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | import static com.github.cstettler.dddttc.accounting.domain.BookingId.bookingId; 8 | 9 | @ValueObject 10 | public class Booking { 11 | 12 | private final BookingId id; 13 | private final WalletOwner walletOwner; 14 | private final long durationInSeconds; 15 | 16 | private Booking(WalletOwner walletOwner, LocalDateTime startedAt, long durationInSeconds) { 17 | this.id = bookingId(walletOwner, startedAt); 18 | this.walletOwner = walletOwner; 19 | this.durationInSeconds = durationInSeconds; 20 | } 21 | 22 | public BookingId id() { 23 | return this.id; 24 | } 25 | 26 | public WalletOwner walletOwner() { 27 | return this.walletOwner; 28 | } 29 | 30 | public long durationInSeconds() { 31 | return this.durationInSeconds; 32 | } 33 | 34 | public static Booking booking(WalletOwner walletOwner, LocalDateTime startedAt, long durationInSeconds) { 35 | return new Booking(walletOwner, startedAt, durationInSeconds); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/web/ModelAndViewBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.web; 2 | 3 | import org.springframework.web.servlet.ModelAndView; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class ModelAndViewBuilder { 9 | 10 | private final Map modelProperties; 11 | private final String viewName; 12 | 13 | private ModelAndViewBuilder(String viewName) { 14 | this.viewName = viewName; 15 | this.modelProperties = new HashMap<>(); 16 | } 17 | 18 | public ModelAndViewBuilder property(String name, Object value) { 19 | this.modelProperties.put(name, value); 20 | 21 | return this; 22 | } 23 | 24 | public ModelAndViewBuilder error(Exception e) { 25 | this.modelProperties.put("error", e.getMessage()); 26 | 27 | return this; 28 | } 29 | 30 | public ModelAndView build() { 31 | return new ModelAndView(this.viewName, this.modelProperties); 32 | } 33 | 34 | public static ModelAndViewBuilder modelAndView(String viewName) { 35 | return new ModelAndViewBuilder(viewName); 36 | } 37 | 38 | public static ModelAndView redirectTo(String url) { 39 | return modelAndView("redirect:" + url).build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventTypeMappings.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import static java.util.Collections.unmodifiableMap; 7 | 8 | public class DomainEventTypeMappings { 9 | 10 | private final Map, String> mappings; 11 | 12 | private DomainEventTypeMappings(Map, String> mappings) { 13 | this.mappings = unmodifiableMap(mappings); 14 | } 15 | 16 | String getDomainEventTypeIfAvailable(Class domainEventClass) { 17 | return this.mappings.get(domainEventClass); 18 | } 19 | 20 | public static Builder domainEventTypeMappings() { 21 | return new Builder(); 22 | } 23 | 24 | 25 | public static class Builder { 26 | 27 | private final Map, String> mappings; 28 | 29 | private Builder() { 30 | this.mappings = new HashMap<>(); 31 | } 32 | 33 | public Builder addMapping(Class domainEventClass, String domainEventType) { 34 | this.mappings.put(domainEventClass, domainEventType); 35 | 36 | return this; 37 | } 38 | 39 | public DomainEventTypeMappings build() { 40 | return new DomainEventTypeMappings(this.mappings); 41 | } 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/infrastructure/persistence/InMemoryBikeRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.infrastructure.persistence; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.Bike; 4 | import com.github.cstettler.dddttc.rental.domain.bike.BikeRepository; 5 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 6 | 7 | import java.util.Collection; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static com.github.cstettler.dddttc.rental.domain.bike.BikeNotExistingException.bikeNotExisting; 12 | 13 | class InMemoryBikeRepository implements BikeRepository { 14 | 15 | private final Map bikeByNumberPlate; 16 | 17 | InMemoryBikeRepository() { 18 | this.bikeByNumberPlate = new HashMap<>(); 19 | } 20 | 21 | @Override 22 | public void add(Bike bike) { 23 | this.bikeByNumberPlate.put(bike.numberPlate(), bike); 24 | } 25 | 26 | @Override 27 | public Bike get(NumberPlate numberPlate) { 28 | Bike bike = this.bikeByNumberPlate.get(numberPlate); 29 | 30 | if (bike == null) { 31 | throw bikeNotExisting(numberPlate); 32 | } 33 | 34 | return bike; 35 | } 36 | 37 | @Override 38 | public Collection findAll() { 39 | return this.bikeByNumberPlate.values(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/resources/templates/list-wallets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Accounting 6 | 7 | 8 | 9 | 10 | 11 | 12 |

15 | 16 |
17 |

Wallets and Balances

18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/VerificationCode.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | 5 | import static java.lang.String.valueOf; 6 | import static java.util.stream.Collectors.joining; 7 | import static java.util.stream.IntStream.range; 8 | 9 | @ValueObject 10 | public class VerificationCode { 11 | 12 | private final String value; 13 | 14 | private VerificationCode() { 15 | this(pseudoRandomNumericString(6)); 16 | } 17 | 18 | private VerificationCode(String value) { 19 | this.value = value; 20 | } 21 | 22 | String value() { 23 | return this.value; 24 | } 25 | 26 | boolean matches(VerificationCode verificationCode) { 27 | return this.value.equals(verificationCode.value()); 28 | } 29 | 30 | public static VerificationCode verificationCode(String value) { 31 | return new VerificationCode(value); 32 | } 33 | 34 | static VerificationCode randomVerificationCode() { 35 | return new VerificationCode(); 36 | } 37 | 38 | private static String pseudoRandomNumericString(int length) { 39 | return range(0, length) 40 | .map((i) -> (int) (Math.random() * 10 % 10)) 41 | .mapToObj((number) -> valueOf(number)) 42 | .collect(joining()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/domain/RecordingDomainEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.domain; 2 | 3 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions.ExcludeFromComponentScan; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @ExcludeFromComponentScan 9 | public class RecordingDomainEventPublisher implements DomainEventPublisher { 10 | 11 | private final List recordedDomainEvents; 12 | 13 | private RecordingDomainEventPublisher() { 14 | this.recordedDomainEvents = new ArrayList<>(); 15 | } 16 | 17 | @Override 18 | public void publish(Object domainEvent) { 19 | this.recordedDomainEvents.add(domainEvent); 20 | } 21 | 22 | public int numberOfRecordedDomainEvents() { 23 | return this.recordedDomainEvents.size(); 24 | } 25 | 26 | public T singleRecordedDomainEvent() { 27 | return recordedDomainEvent(0); 28 | } 29 | 30 | @SuppressWarnings("unchecked") 31 | public T recordedDomainEvent(int index) { 32 | return (T) this.recordedDomainEvents.get(index); 33 | } 34 | 35 | public void clearRecordedDomainEvents() { 36 | this.recordedDomainEvents.clear(); 37 | } 38 | 39 | public static RecordingDomainEventPublisher recordingDomainEventPublisher() { 40 | return new RecordingDomainEventPublisher(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /etc/architecture-governance/constraints.adoc: -------------------------------------------------------------------------------- 1 | == Constraints 2 | 3 | [[dddttc:onion]] 4 | [role=group,includesConcepts="dddttc:*",includesConstraints="dddttc:*"] 5 | 6 | The following constraints are enforced in the code base: 7 | 8 | 9 | === C1 - Aggregates must reside in Domain Ring 10 | 11 | As aggregates contain business logic and related state, and only the domain ring must contain business logic, aggregates 12 | must reside in the domain ring. 13 | 14 | _jQAssistant Constraint Definition_ 15 | [[dddttc:AggregateMustResideInDomainRingConstraint]] 16 | .Aggregates must reside in the domain ring. 17 | [source,cypher,role=constraint,requiresConcepts="dddttc:AggregateConcept,dddttc:DomainRingConcept"] 18 | ---- 19 | MATCH (a:Aggregate) 20 | WHERE NOT a:DomainRing 21 | RETURN a.fqn AS Aggregate 22 | ---- 23 | 24 | 25 | === C2 - Aggregates must not use Application Services 26 | 27 | As application services represent the boundary of the domain towards external contexts, aggregates must not invoke 28 | application services. 29 | 30 | _jQAssistant Constraint Definition_ 31 | [[dddttc:AggregatesMustNotUseApplicationServicesConstraint]] 32 | .Aggregates must not use application services. 33 | [source,cypher,role=constraint,requiresConcepts="dddttc:AggregateConcept,dddttc:ApplicationServiceConcept"] 34 | ---- 35 | MATCH (a:Aggregate)-[:DEPENDS_ON]->(s:ApplicationService) 36 | RETURN a.fqn AS Aggregate, s.fqn AS ApplicationService 37 | ---- 38 | 39 | (more constraints to follow) 40 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookingCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 4 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 5 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 6 | 7 | @DomainEvent 8 | public class BookingCompletedEvent { 9 | 10 | private final BookingId bookingId; 11 | private final NumberPlate numberPlate; 12 | private final UserId userId; 13 | private final BikeUsage bikeUsage; 14 | 15 | private BookingCompletedEvent(BookingId bookingId, NumberPlate numberPlate, UserId userId, BikeUsage bikeUsage) { 16 | this.bookingId = bookingId; 17 | this.numberPlate = numberPlate; 18 | this.userId = userId; 19 | this.bikeUsage = bikeUsage; 20 | } 21 | 22 | BookingId bookingId() { 23 | return this.bookingId; 24 | } 25 | 26 | public NumberPlate numberPlate() { 27 | return this.numberPlate; 28 | } 29 | 30 | public UserId userId() { 31 | return this.userId; 32 | } 33 | 34 | BikeUsage bikeUsage() { 35 | return this.bikeUsage; 36 | } 37 | 38 | static BookingCompletedEvent bookingCompleted(BookingId bookingId, NumberPlate numberPlate, UserId userId, BikeUsage bikeUsage) { 39 | return new BookingCompletedEvent(bookingId, numberPlate, userId, bikeUsage); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | 5 | @DomainEvent 6 | public class UserRegistrationCompletedEvent { 7 | 8 | private final UserRegistrationId userRegistrationId; 9 | private final UserHandle userHandle; 10 | private final PhoneNumber phoneNumber; 11 | private final FullName fullName; 12 | 13 | private UserRegistrationCompletedEvent(UserRegistrationId userRegistrationId, UserHandle userHandle, PhoneNumber phoneNumber, FullName fullName) { 14 | this.userRegistrationId = userRegistrationId; 15 | this.userHandle = userHandle; 16 | this.phoneNumber = phoneNumber; 17 | this.fullName = fullName; 18 | } 19 | 20 | public UserRegistrationId userRegistrationId() { 21 | return this.userRegistrationId; 22 | } 23 | 24 | public UserHandle userHandle() { 25 | return this.userHandle; 26 | } 27 | 28 | public PhoneNumber phoneNumber() { 29 | return this.phoneNumber; 30 | } 31 | 32 | public FullName fullName() { 33 | return this.fullName; 34 | } 35 | 36 | public static UserRegistrationCompletedEvent userRegistrationCompleted(UserRegistrationId userRegistrationId, UserHandle userHandle, PhoneNumber phoneNumber, FullName fullName) { 37 | return new UserRegistrationCompletedEvent(userRegistrationId, userHandle, phoneNumber, fullName); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/VerificationCodeTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Set; 6 | 7 | import static com.github.cstettler.dddttc.registration.domain.VerificationCode.randomVerificationCode; 8 | import static java.util.stream.Collectors.toSet; 9 | import static java.util.stream.IntStream.range; 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.greaterThan; 12 | import static org.hamcrest.Matchers.is; 13 | 14 | class VerificationCodeTests { 15 | 16 | @Test 17 | void randomVerificationCode_anyState_returnsSixDigitsNumericString() { 18 | // act 19 | VerificationCode verificationCode = randomVerificationCode(); 20 | 21 | // assert 22 | assertThat(verificationCode.value().length(), is(6)); 23 | } 24 | 25 | @Test 26 | void randomVerificationCode_multipleInvocations_generatesNearlyUniqueVerificationCodes() { 27 | // arrange 28 | int numberOfGeneratedVerificationCodes = 10000; 29 | 30 | // act 31 | Set uniqueVerificationCodeValues = range(0, numberOfGeneratedVerificationCodes) 32 | .mapToObj((index) -> randomVerificationCode()) 33 | .map((verificationCode) -> verificationCode.value()) 34 | .collect(toSet()); 35 | 36 | // assert 37 | assertThat(uniqueVerificationCodeValues.size(), greaterThan(numberOfGeneratedVerificationCodes - 100)); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/EnableComponentScanExclusions.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | 4 | import org.springframework.boot.context.TypeExcludeFilter; 5 | import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; 6 | import org.springframework.core.type.classreading.MetadataReader; 7 | import org.springframework.core.type.classreading.MetadataReaderFactory; 8 | 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.Target; 11 | 12 | import static java.lang.annotation.ElementType.TYPE; 13 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 14 | 15 | @Target(TYPE) 16 | @Retention(RUNTIME) 17 | @TypeExcludeFilters(EnableComponentScanExclusions.ComponentScanExcludeFilter.class) 18 | public @interface EnableComponentScanExclusions { 19 | 20 | @Target(TYPE) 21 | @Retention(RUNTIME) 22 | @interface ExcludeFromComponentScan { 23 | 24 | } 25 | 26 | 27 | class ComponentScanExcludeFilter extends TypeExcludeFilter { 28 | 29 | @Override 30 | public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { 31 | return metadataReader.getAnnotationMetadata().hasAnnotation(ExcludeFromComponentScan.class.getName()); 32 | } 33 | 34 | @Override 35 | public boolean equals(Object other) { 36 | return other instanceof ComponentScanExcludeFilter; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return 37; 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/templates/list-bikes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rental 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Bikes

18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/test/DatabaseCleaner.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.test; 2 | 3 | import org.junit.jupiter.api.extension.AfterEachCallback; 4 | import org.junit.jupiter.api.extension.ExtensionContext; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | 7 | import javax.sql.DataSource; 8 | import java.sql.DatabaseMetaData; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | 12 | import static org.springframework.jdbc.support.JdbcUtils.extractDatabaseMetaData; 13 | import static org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext; 14 | 15 | public class DatabaseCleaner implements AfterEachCallback { 16 | 17 | @SuppressWarnings("ConstantConditions") 18 | @Override 19 | public void afterEach(ExtensionContext context) throws Exception { 20 | JdbcTemplate jdbcTemplate = getApplicationContext(context).getBean(JdbcTemplate.class); 21 | DataSource dataSource = jdbcTemplate.getDataSource(); 22 | 23 | extractDatabaseMetaData(dataSource, (databaseMetaData) -> { 24 | truncateTables(jdbcTemplate, databaseMetaData); 25 | 26 | return null; 27 | }); 28 | } 29 | 30 | @SuppressWarnings("SqlResolve") 31 | private static void truncateTables(JdbcTemplate jdbcTemplate, DatabaseMetaData databaseMetaData) throws SQLException { 32 | ResultSet resultSet = databaseMetaData.getTables(null, null, "%", new String[]{"TABLE"}); 33 | 34 | while (resultSet.next()) { 35 | String tableName = resultSet.getString(3); 36 | 37 | jdbcTemplate.execute("DELETE FROM \"" + tableName + "\""); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/templates/done.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Registration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

User Registration Complete

18 | 19 |
User registration successfully completed
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-aspect/src/main/java/com/github/cstettler/dddttc/aspect/StateBasedEqualsAndHashCode.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.aspect; 2 | 3 | import java.lang.reflect.Field; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | 8 | public class StateBasedEqualsAndHashCode { 9 | 10 | private StateBasedEqualsAndHashCode() { 11 | } 12 | 13 | public static boolean stateBasedEquals(Object object, Object other) { 14 | if (object == other) { 15 | return true; 16 | } 17 | 18 | if (other == null || object.getClass() != other.getClass()) { 19 | return false; 20 | } 21 | 22 | return Objects.equals(stateProperties(object), stateProperties(other)); 23 | } 24 | 25 | public static int stateBasedHashCode(Object object) { 26 | return Objects.hash(stateProperties(object)); 27 | } 28 | 29 | private static Map stateProperties(Object object) { 30 | try { 31 | // TODO for value objects, consider all declared fields in hierarchy 32 | Map stateProperties = new HashMap<>(); 33 | Field[] declaredFields = object.getClass().getDeclaredFields(); 34 | 35 | for (Field declaredField : declaredFields) { 36 | declaredField.setAccessible(true); 37 | Object value = declaredField.get(object); 38 | 39 | stateProperties.put(declaredField.getName(), value); 40 | } 41 | 42 | return stateProperties; 43 | } catch (Exception e) { 44 | throw new IllegalStateException("unable to get state properties of object '" + object + "'", e); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/PhoneNumberTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.cstettler.dddttc.registration.domain.PhoneNumber.phoneNumber; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.is; 8 | 9 | class PhoneNumberTests { 10 | 11 | @Test 12 | void isSwiss_phoneNumberWithPlusFourtyOnePrefix_returnsTrue() { 13 | // arrange 14 | PhoneNumber phoneNumber = phoneNumber("+4179123456"); 15 | 16 | // act 17 | boolean swiss = phoneNumber.isSwiss(); 18 | 19 | // assert 20 | assertThat(swiss, is(true)); 21 | } 22 | 23 | @Test 24 | void isSwiss_phoneNumberWithZeroFourtyOnePrefix_returnsTrue() { 25 | // arrange 26 | PhoneNumber phoneNumber = phoneNumber("004179123456"); 27 | 28 | // act 29 | boolean swiss = phoneNumber.isSwiss(); 30 | 31 | // assert 32 | assertThat(swiss, is(true)); 33 | } 34 | 35 | @Test 36 | void isSwiss_phoneNumberWithAnyOtherPrefix_returnsFalse() { 37 | // arrange 38 | PhoneNumber phoneNumber = phoneNumber("+42791234567"); 39 | 40 | // act 41 | boolean swiss = phoneNumber.isSwiss(); 42 | 43 | // assert 44 | assertThat(swiss, is(false)); 45 | } 46 | 47 | @Test 48 | void isSwiss_phoneNumberWithNullValue_returnsFalse() { 49 | // arrange 50 | PhoneNumber phoneNumber = phoneNumber(null); 51 | 52 | // act 53 | boolean swiss = phoneNumber.isSwiss(); 54 | 55 | // assert 56 | assertThat(swiss, is(false)); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationCompletedEventMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.support.ReflectionBasedStateMatcher; 4 | import org.hamcrest.Matcher; 5 | 6 | import java.util.Map; 7 | 8 | public class UserRegistrationCompletedEventMatcher extends ReflectionBasedStateMatcher { 9 | 10 | private UserRegistrationCompletedEventMatcher(Map> matchersByPropertyName) { 11 | super("user registration completed event", matchersByPropertyName); 12 | } 13 | 14 | public static Builder userRegistrationCompletedEventWith() { 15 | return new Builder(); 16 | } 17 | 18 | 19 | public static class Builder extends MatcherBuilder { 20 | 21 | public Builder id(Matcher idMatcher) { 22 | return recordPropertyMatcher(this, "id", idMatcher); 23 | } 24 | 25 | public Builder userHandle(Matcher userHandleMatcher) { 26 | return recordPropertyMatcher(this, "userHandle", userHandleMatcher); 27 | } 28 | 29 | public Builder phoneNumber(Matcher phoneNumberMatcher) { 30 | return recordPropertyMatcher(this, "phoneNumber", phoneNumberMatcher); 31 | } 32 | 33 | public Builder fullName(Matcher fullNameMatcher) { 34 | return recordPropertyMatcher(this, "fullName", fullNameMatcher); 35 | } 36 | 37 | @Override 38 | protected UserRegistrationCompletedEventMatcher build(Map> matchersByPropertyName) { 39 | return new UserRegistrationCompletedEventMatcher(matchersByPropertyName); 40 | } 41 | 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/SendVerificationCodeSmsServiceTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.cstettler.dddttc.registration.domain.PhoneNumber.phoneNumber; 6 | import static com.github.cstettler.dddttc.registration.domain.PhoneNumberVerificationCodeGeneratedEvent.phoneNumberVerificationCodeGenerated; 7 | import static com.github.cstettler.dddttc.registration.domain.VerificationCode.verificationCode; 8 | import static org.mockito.ArgumentMatchers.contains; 9 | import static org.mockito.ArgumentMatchers.eq; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.verify; 12 | 13 | class SendVerificationCodeSmsServiceTests { 14 | 15 | @Test 16 | void sendVerificationCodeSmsToPhoneNumber_verificationCodeGeneratedEventReceived_invokesSmsNotificationService() { 17 | // arrange 18 | PhoneNumber phoneNumber = phoneNumber("+41 79 123 45 67"); 19 | VerificationCode verificationCode = verificationCode("123456"); 20 | PhoneNumberVerificationCodeGeneratedEvent event = phoneNumberVerificationCodeGenerated(phoneNumber, verificationCode); 21 | 22 | SmsNotificationSender smsNotificationSender = smsNotificationSender(); 23 | SendVerificationCodeSmsService sendVerificationCodeSmsService = new SendVerificationCodeSmsService(smsNotificationSender); 24 | 25 | // act 26 | sendVerificationCodeSmsService.sendVerificationCodeSmsToPhoneNumber(event); 27 | 28 | // assert 29 | verify(smsNotificationSender).sendSmsTo(eq(phoneNumber), contains("123456")); 30 | } 31 | 32 | private static SmsNotificationSender smsNotificationSender() { 33 | return mock(SmsNotificationSender.class); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/templates/book-bike.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rental 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Book Bike

18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/templates/start.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Registration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Start User Registration

18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/infrastructure/event/UserRegistrationCompletedMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.accounting.application.WalletService; 4 | import com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.jms.annotation.JmsListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | import static com.github.cstettler.dddttc.accounting.domain.WalletOwner.walletOwner; 11 | 12 | @Component 13 | class UserRegistrationCompletedMessageListener { 14 | 15 | private static Logger LOGGER = LoggerFactory.getLogger(UserRegistrationCompletedMessageListener.class); 16 | 17 | private final WalletService walletService; 18 | 19 | UserRegistrationCompletedMessageListener(WalletService walletService) { 20 | this.walletService = walletService; 21 | } 22 | 23 | @JmsListener(destination = "registration/user-registration-completed") 24 | public void onUserRegistrationCompleted(UserRegistrationCompletedMessage message) { 25 | String userHandle = message.userHandle.value; 26 | 27 | LOGGER.info("received user registration completed message for user '" + userHandle + "'"); 28 | 29 | try { 30 | this.walletService.initializeWallet(walletOwner(userHandle)); 31 | 32 | LOGGER.info("initialized wallet for user handle '" + userHandle + "'"); 33 | } catch (WalletAlreadyExistsException e) { 34 | // ignored 35 | } 36 | } 37 | 38 | 39 | public static class UserRegistrationCompletedMessage { 40 | 41 | ValueWrapper userHandle; 42 | 43 | } 44 | 45 | 46 | public static class ValueWrapper { 47 | 48 | String value; 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/ChargeWelcomeAmountToWalletService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEventHandler; 4 | import com.github.cstettler.dddttc.stereotype.DomainService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import static com.github.cstettler.dddttc.accounting.domain.Amount.amount; 11 | import static com.github.cstettler.dddttc.accounting.domain.TransactionReference.transactionReference; 12 | 13 | @DomainService 14 | class ChargeWelcomeAmountToWalletService { 15 | 16 | private static Logger LOGGER = LoggerFactory.getLogger(ChargeWelcomeAmountToWalletService.class); 17 | 18 | private final WalletRepository walletRepository; 19 | private final Amount welcomeAmount; 20 | 21 | ChargeWelcomeAmountToWalletService(WalletRepository walletRepository) { 22 | this.walletRepository = walletRepository; 23 | this.welcomeAmount = amount(new BigDecimal("5.00")); 24 | } 25 | 26 | @DomainEventHandler 27 | void chargeWelcomeAmountToWallet(WalletInitializedEvent event) { 28 | try { 29 | WalletOwner walletOwner = event.walletOwner(); 30 | TransactionReference welcomeAmountTransactionReference = transactionReference("welcome-" + walletOwner.value()); 31 | 32 | Wallet wallet = this.walletRepository.get(walletOwner); 33 | wallet.chargeAmount(this.welcomeAmount, welcomeAmountTransactionReference); 34 | 35 | this.walletRepository.update(wallet); 36 | 37 | LOGGER.info("charged welcome amount of '" + this.welcomeAmount.value() + "' to wallet of owner '" + walletOwner.value() + "'"); 38 | } catch (TransactionWithSameReferenceAlreadyAppliedException e) { 39 | // ignored 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventSerializer.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 6 | 7 | import java.io.IOException; 8 | 9 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; 10 | import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD; 11 | import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; 12 | 13 | class DomainEventSerializer { 14 | 15 | private final ObjectMapper objectMapper; 16 | 17 | DomainEventSerializer() { 18 | this.objectMapper = fieldAccessEnabledObjectMapper(); 19 | } 20 | 21 | String serialize(Object domainEvent) { 22 | try { 23 | return this.objectMapper.writeValueAsString(domainEvent); 24 | } catch (JsonProcessingException e) { 25 | throw new IllegalStateException("unable to serialize domain event '" + domainEvent + "'", e); 26 | } 27 | } 28 | 29 | T deserialize(String payload, Class type) { 30 | try { 31 | return this.objectMapper.readValue(payload, type); 32 | } catch (IOException e) { 33 | throw new IllegalStateException("unable to deserialize domain event payload '" + payload + "' to type '" + type.getName() + "'", e); 34 | } 35 | } 36 | 37 | private static ObjectMapper fieldAccessEnabledObjectMapper() { 38 | ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder() 39 | .featuresToDisable(WRITE_DATES_AS_TIMESTAMPS) 40 | .build(); 41 | 42 | objectMapper.setVisibility(FIELD, ANY); 43 | 44 | return objectMapper; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/resources/templates/list-bookings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rental 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Bookings

18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/BookBikeService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.Bike; 4 | import com.github.cstettler.dddttc.rental.domain.bike.BikeAlreadyBookedException; 5 | import com.github.cstettler.dddttc.rental.domain.bike.BikeNotExistingException; 6 | import com.github.cstettler.dddttc.rental.domain.bike.BikeRepository; 7 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 8 | import com.github.cstettler.dddttc.rental.domain.user.User; 9 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 10 | import com.github.cstettler.dddttc.rental.domain.user.UserNotExistingException; 11 | import com.github.cstettler.dddttc.rental.domain.user.UserRepository; 12 | import com.github.cstettler.dddttc.stereotype.DomainService; 13 | 14 | import java.time.Clock; 15 | 16 | @DomainService 17 | public class BookBikeService { 18 | 19 | private final BikeRepository bikeRepository; 20 | private final BookingRepository bookingRepository; 21 | private final UserRepository userRepository; 22 | private final Clock clock; 23 | 24 | public BookBikeService(BikeRepository bikeRepository, BookingRepository bookingRepository, UserRepository userRepository, Clock clock) { 25 | this.bikeRepository = bikeRepository; 26 | this.bookingRepository = bookingRepository; 27 | this.userRepository = userRepository; 28 | this.clock = clock; 29 | } 30 | 31 | public void bookBike(NumberPlate numberPlate, UserId userId) throws BikeNotExistingException, UserNotExistingException, BikeAlreadyBookedException { 32 | Bike bike = this.bikeRepository.get(numberPlate); 33 | User user = this.userRepository.get(userId); 34 | 35 | Booking booking = bike.bookBikeFor(user, this.clock); 36 | this.bikeRepository.add(bike); 37 | this.bookingRepository.add(booking); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/application/BookingService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.application; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.BikeAlreadyBookedException; 4 | import com.github.cstettler.dddttc.rental.domain.bike.BikeNotExistingException; 5 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 6 | import com.github.cstettler.dddttc.rental.domain.booking.BookBikeService; 7 | import com.github.cstettler.dddttc.rental.domain.booking.Booking; 8 | import com.github.cstettler.dddttc.rental.domain.booking.BookingId; 9 | import com.github.cstettler.dddttc.rental.domain.booking.BookingRepository; 10 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 11 | import com.github.cstettler.dddttc.rental.domain.user.UserNotExistingException; 12 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 13 | 14 | import java.time.Clock; 15 | import java.util.Collection; 16 | 17 | @ApplicationService 18 | public class BookingService { 19 | 20 | private final BookBikeService bookBikeService; 21 | private final BookingRepository bookingRepository; 22 | private final Clock clock; 23 | 24 | BookingService(BookBikeService bookBikeService, BookingRepository bookingRepository, Clock clock) { 25 | this.bookBikeService = bookBikeService; 26 | this.bookingRepository = bookingRepository; 27 | this.clock = clock; 28 | } 29 | 30 | public void bookBike(NumberPlate numberPlate, UserId userId) throws BikeNotExistingException, UserNotExistingException, BikeAlreadyBookedException { 31 | this.bookBikeService.bookBike(numberPlate, userId); 32 | } 33 | 34 | public void returnBike(BookingId bookingId) { 35 | Booking booking = this.bookingRepository.get(bookingId); 36 | booking.returnBike(this.clock); 37 | 38 | this.bookingRepository.update(booking); 39 | } 40 | 41 | public Collection listBookings() { 42 | return this.bookingRepository.findAll(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/templates/verify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Registration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Verify Phone Number

18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/application/WalletService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.application; 2 | 3 | import com.github.cstettler.dddttc.accounting.domain.Booking; 4 | import com.github.cstettler.dddttc.accounting.domain.BookingAlreadyBilledException; 5 | import com.github.cstettler.dddttc.accounting.domain.BookingFeePolicy; 6 | import com.github.cstettler.dddttc.accounting.domain.Wallet; 7 | import com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException; 8 | import com.github.cstettler.dddttc.accounting.domain.WalletOwner; 9 | import com.github.cstettler.dddttc.accounting.domain.WalletRepository; 10 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 11 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 12 | 13 | import java.util.Collection; 14 | 15 | import static com.github.cstettler.dddttc.accounting.domain.Wallet.newWallet; 16 | 17 | @ApplicationService 18 | public class WalletService { 19 | 20 | private final BookingFeePolicy bookingFeePolicy; 21 | private final WalletRepository walletRepository; 22 | private final DomainEventPublisher domainEventPublisher; 23 | 24 | WalletService(BookingFeePolicy bookingFeePolicy, WalletRepository walletRepository, DomainEventPublisher domainEventPublisher) { 25 | this.bookingFeePolicy = bookingFeePolicy; 26 | this.walletRepository = walletRepository; 27 | this.domainEventPublisher = domainEventPublisher; 28 | } 29 | 30 | public void initializeWallet(WalletOwner walletOwner) throws WalletAlreadyExistsException { 31 | Wallet wallet = newWallet(walletOwner, this.domainEventPublisher); 32 | 33 | this.walletRepository.add(wallet); 34 | } 35 | 36 | public void billBookingFee(Booking booking) throws BookingAlreadyBilledException { 37 | Wallet wallet = this.walletRepository.get(booking.walletOwner()); 38 | wallet.billBookingFee(booking, this.bookingFeePolicy); 39 | 40 | this.walletRepository.update(wallet); 41 | } 42 | 43 | public Collection listWallets() { 44 | return this.walletRepository.findAll(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationIdTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Set; 6 | 7 | import static com.github.cstettler.dddttc.registration.domain.UserRegistrationId.newUserRegistrationId; 8 | import static com.github.cstettler.dddttc.registration.domain.UserRegistrationId.userRegistrationId; 9 | import static java.util.stream.Collectors.toSet; 10 | import static java.util.stream.IntStream.range; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.Matchers.equalTo; 13 | import static org.hamcrest.Matchers.is; 14 | import static org.hamcrest.Matchers.not; 15 | import static org.hamcrest.Matchers.nullValue; 16 | 17 | class UserRegistrationIdTests { 18 | 19 | @Test 20 | void newUserRegistrationId_anyState_createsNewUserRegistrationIdWithInternalValue() { 21 | // act 22 | UserRegistrationId userRegistrationId = newUserRegistrationId(); 23 | 24 | // assert 25 | assertThat(userRegistrationId, is(not(nullValue()))); 26 | assertThat(userRegistrationId.value(), is(not(nullValue()))); 27 | } 28 | 29 | @Test 30 | void newUserRegistrationId_multipleInvocations_generatesUniqueUserRegistrationIdValues() { 31 | // arrange 32 | int numberOfCreatedUserRegistrationIds = 10000; 33 | 34 | // act 35 | Set uniqueUserRegistrationIdValues = range(0, numberOfCreatedUserRegistrationIds) 36 | .mapToObj((index) -> newUserRegistrationId()) 37 | .map((userRegistrationId) -> userRegistrationId.value()) 38 | .collect(toSet()); 39 | 40 | // assert 41 | assertThat(uniqueUserRegistrationIdValues.size(), is(numberOfCreatedUserRegistrationIds)); 42 | } 43 | 44 | @Test 45 | void userRegistrationId_valueProvided_inflatesUserRegistrationIdWithProvidedValue() { 46 | // act 47 | UserRegistrationId userRegistration = userRegistrationId("id-value"); 48 | 49 | // assert 50 | assertThat(userRegistration, is(not(nullValue()))); 51 | assertThat(userRegistration.value(), is(equalTo("id-value"))); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | import org.objenesis.ObjenesisStd; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.Map; 7 | 8 | public class ReflectionUtils { 9 | 10 | private static final ObjenesisStd OBJENESIS = new ObjenesisStd(); 11 | 12 | private ReflectionUtils() { 13 | } 14 | 15 | public static T newInstance(Class type) { 16 | return OBJENESIS.newInstance(type); 17 | } 18 | 19 | public static T newInstance(Class type, Map properties) { 20 | T instance = newInstance(type); 21 | properties.forEach((propertyName, propertyValue) -> applyProperty(instance, propertyName, propertyValue)); 22 | 23 | return instance; 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | public static T propertyValue(Object target, String propertyName) { 28 | try { 29 | Field propertyField = target.getClass().getDeclaredField(propertyName); 30 | propertyField.setAccessible(true); 31 | Object propertyValue = propertyField.get(target); 32 | 33 | return (T) propertyValue; 34 | } catch (NoSuchFieldException e) { 35 | throw new IllegalStateException("property '" + propertyName + "' not found on target '" + target + "'"); 36 | } catch (Exception e) { 37 | throw new IllegalStateException("unable to get value of property '" + propertyName + "' on target '" + target + "'", e); 38 | } 39 | } 40 | 41 | public static void applyProperty(Object target, String propertyName, Object propertyValue) { 42 | try { 43 | Field propertyField = target.getClass().getDeclaredField(propertyName); 44 | propertyField.setAccessible(true); 45 | propertyField.set(target, propertyValue); 46 | } catch (NoSuchFieldException e) { 47 | throw new IllegalStateException("property '" + propertyName + "' not found on target '" + target + "'"); 48 | } catch (Exception e) { 49 | throw new IllegalStateException("unable to set value of property '" + propertyName + "' on target '" + target + "'", e); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-aspect/src/main/java/com/github/cstettler/dddttc/aspect/DefaultConstructorAspectAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.aspect; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Aggregate; 4 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 5 | import com.github.cstettler.dddttc.stereotype.ValueObject; 6 | import com.google.auto.service.AutoService; 7 | 8 | import javax.annotation.processing.Processor; 9 | import javax.lang.model.element.TypeElement; 10 | import java.io.PrintWriter; 11 | import java.lang.annotation.Annotation; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | import static java.util.Arrays.asList; 16 | 17 | @AutoService(Processor.class) 18 | public class DefaultConstructorAspectAnnotationProcessor extends StereotypeBasedAspectAnnotationProcessor { 19 | 20 | @Override 21 | protected Set> supportedAnnotations() { 22 | return new HashSet<>(asList( 23 | Aggregate.class, 24 | ValueObject.class, 25 | DomainEvent.class 26 | )); 27 | } 28 | 29 | @Override 30 | protected String aspectDescription() { 31 | return "default constructor"; 32 | } 33 | 34 | @Override 35 | protected String aspectName(String simpleName) { 36 | return simpleName + "ProtectedDefaultConstructorAspect"; 37 | } 38 | 39 | @Override 40 | protected boolean shouldWriteAspectFor(TypeElement annotatedElement) { 41 | return !(hasDeclaredMethod(annotatedElement, "")); 42 | } 43 | 44 | @Override 45 | protected void writeAspectCode(PrintWriter writer, String aspectName, TypeElement typeElement) { 46 | writer.println("package " + elementPackage(typeElement) + ";"); 47 | writer.println(""); 48 | writer.println("import " + fullyQualifiedElementName(typeElement) + ";"); 49 | writer.println(""); 50 | writer.println("public aspect " + aspectName + " {"); 51 | writer.println(""); 52 | writer.println(" public " + simpleName(typeElement) + ".new() {"); 53 | writer.println(" // e.g. for jpa or jackson deserializer"); 54 | writer.println(" }"); 55 | writer.println(""); 56 | writer.println("}"); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/test/java/com/github/cstettler/dddttc/registration/domain/UserRegistrationMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.domain; 2 | 3 | import com.github.cstettler.dddttc.support.ReflectionBasedStateMatcher; 4 | import org.hamcrest.Matcher; 5 | 6 | import java.util.Map; 7 | 8 | public class UserRegistrationMatcher extends ReflectionBasedStateMatcher { 9 | 10 | private UserRegistrationMatcher(Map> matchersByPropertyName) { 11 | super("user registration", matchersByPropertyName); 12 | } 13 | 14 | public static Builder userRegistrationWith() { 15 | return new Builder(); 16 | } 17 | 18 | 19 | public static class Builder extends MatcherBuilder { 20 | 21 | public Builder id(Matcher idMatcher) { 22 | return recordPropertyMatcher(this, "id", idMatcher); 23 | } 24 | 25 | public Builder userHandle(Matcher userHandleMatcher) { 26 | return recordPropertyMatcher(this, "userHandle", userHandleMatcher); 27 | } 28 | 29 | public Builder phoneNumber(Matcher phoneNumberMatcher) { 30 | return recordPropertyMatcher(this, "phoneNumber", phoneNumberMatcher); 31 | } 32 | 33 | public Builder phoneNumberVerificationCode(Matcher phoneNumberVerificationCodeMatcher) { 34 | return recordPropertyMatcher(this, "phoneNumberVerificationCode", phoneNumberVerificationCodeMatcher); 35 | } 36 | 37 | public Builder phoneNumberVerified(Matcher phoneNumberVerifiedMatcher) { 38 | return recordPropertyMatcher(this, "phoneNumberVerified", phoneNumberVerifiedMatcher); 39 | } 40 | 41 | public Builder fullName(Matcher fullNameMatcher) { 42 | return recordPropertyMatcher(this, "fullName", fullNameMatcher); 43 | } 44 | 45 | public Builder completed(Matcher completedMatcher) { 46 | return recordPropertyMatcher(this, "completed", completedMatcher); 47 | } 48 | 49 | @Override 50 | protected UserRegistrationMatcher build(Map> matchersByPropertyName) { 51 | return new UserRegistrationMatcher(matchersByPropertyName); 52 | } 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/infrastructure/event/UserRegistrationCompletedMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.rental.application.UserService; 4 | import com.github.cstettler.dddttc.rental.domain.user.UserAlreadyExistsException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.jms.annotation.JmsListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | import static com.github.cstettler.dddttc.rental.domain.user.FirstName.firstName; 11 | import static com.github.cstettler.dddttc.rental.domain.user.LastName.lastName; 12 | import static com.github.cstettler.dddttc.rental.domain.user.UserId.userId; 13 | 14 | @Component 15 | class UserRegistrationCompletedMessageListener { 16 | 17 | private static Logger LOGGER = LoggerFactory.getLogger(UserRegistrationCompletedMessageListener.class); 18 | 19 | private final UserService userService; 20 | 21 | UserRegistrationCompletedMessageListener(UserService userService) { 22 | this.userService = userService; 23 | } 24 | 25 | @JmsListener(destination = "registration/user-registration-completed") 26 | public void onUserRegistrationCompleted(UserRegistrationCompletedMessage message) { 27 | String userHandle = message.userHandle.value; 28 | String firstName = message.fullName.firstName; 29 | String lastName = message.fullName.lastName; 30 | 31 | LOGGER.info("received user registration completed message for user handle '" + userHandle + "'"); 32 | 33 | try { 34 | this.userService.addUser(userId(userHandle), firstName(firstName), lastName(lastName)); 35 | 36 | LOGGER.info("added user '" + userHandle + "'"); 37 | } catch (UserAlreadyExistsException e) { 38 | // ignored 39 | } 40 | } 41 | 42 | 43 | public static class UserRegistrationCompletedMessage { 44 | 45 | ValueWrapper userHandle; 46 | FullName fullName; 47 | 48 | } 49 | 50 | 51 | public static class FullName { 52 | 53 | String firstName; 54 | String lastName; 55 | 56 | } 57 | 58 | 59 | public static class ValueWrapper { 60 | 61 | String value; 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/bike/Bike.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.bike; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.booking.Booking; 4 | import com.github.cstettler.dddttc.rental.domain.user.User; 5 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 6 | import com.github.cstettler.dddttc.stereotype.Aggregate; 7 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 8 | import com.github.cstettler.dddttc.stereotype.AggregateId; 9 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 10 | 11 | import java.time.Clock; 12 | 13 | import static com.github.cstettler.dddttc.rental.domain.bike.BikeAlreadyBookedException.bikeAlreadyBooked; 14 | import static com.github.cstettler.dddttc.rental.domain.booking.Booking.newBooking; 15 | 16 | @Aggregate 17 | public class Bike { 18 | 19 | private final NumberPlate numberPlate; 20 | private UserId userId; 21 | private transient final DomainEventPublisher domainEventPublisher; 22 | 23 | private Bike(NumberPlate numberPlate, DomainEventPublisher domainEventPublisher) { 24 | this.numberPlate = numberPlate; 25 | this.domainEventPublisher = domainEventPublisher; 26 | 27 | this.userId = null; 28 | } 29 | 30 | @AggregateId 31 | public NumberPlate numberPlate() { 32 | return this.numberPlate; 33 | } 34 | 35 | public boolean available() { 36 | return this.userId == null; 37 | } 38 | 39 | @AggregateFactory(Booking.class) 40 | public Booking bookBikeFor(User user, Clock clock) throws BikeAlreadyBookedException { 41 | if (this.userId != null) { 42 | throw bikeAlreadyBooked(this.numberPlate, this.userId); 43 | } 44 | 45 | this.userId = user.id(); 46 | 47 | return newBooking(this.numberPlate, this.userId, this.domainEventPublisher, clock); 48 | } 49 | 50 | void markAsReturnedBy(UserId userId) { 51 | if (!(this.userId.equals(userId))) { 52 | throw bikeAlreadyBooked(this.numberPlate, this.userId); 53 | } 54 | 55 | this.userId = null; 56 | } 57 | 58 | static Bike newBike(NumberPlate numberPlate, DomainEventPublisher domainEventPublisher) { 59 | return new Bike(numberPlate, domainEventPublisher); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/ReflectionBasedStateBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | import java.security.SecureRandom; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.Random; 9 | 10 | import static com.github.cstettler.dddttc.support.ReflectionUtils.newInstance; 11 | 12 | public abstract class ReflectionBasedStateBuilder { 13 | 14 | private static final Random RANDOM = new SecureRandom(); 15 | private static final char[] RANDOM_STRING_SYMBOLS = randomStringSymbols(); 16 | 17 | private Map properties; 18 | 19 | protected ReflectionBasedStateBuilder() { 20 | this.properties = new LinkedHashMap<>(); 21 | } 22 | 23 | protected > B recordProperty(B builder, String propertyName, Object propertyValue) { 24 | this.properties.put(propertyName, propertyValue); 25 | 26 | return builder; 27 | } 28 | 29 | public T build() { 30 | return newInstance(objectType(), this.properties); 31 | } 32 | 33 | @SuppressWarnings("unchecked") 34 | private Class objectType() { 35 | Type genericSuperclass = getClass().getGenericSuperclass(); 36 | 37 | if (genericSuperclass instanceof ParameterizedType) { 38 | ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; 39 | return (Class) parameterizedType.getActualTypeArguments()[0]; 40 | } 41 | 42 | throw new IllegalStateException("unable to determine object type via generic type parameter of class '" + getClass().getName() + "'"); 43 | } 44 | 45 | protected static String randomString(int length) { 46 | char[] buffer = new char[length]; 47 | 48 | for (int i = 0; i < buffer.length; ++i) { 49 | buffer[i] = RANDOM_STRING_SYMBOLS[RANDOM.nextInt(RANDOM_STRING_SYMBOLS.length)]; 50 | } 51 | 52 | return new String(buffer); 53 | } 54 | 55 | private static char[] randomStringSymbols() { 56 | String upperCaseSymbols = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 57 | String lowerCaseSymbols = upperCaseSymbols.toLowerCase(); 58 | 59 | return (upperCaseSymbols + lowerCaseSymbols).toCharArray(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/infrastructure/web/AccountingController.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.web; 2 | 3 | import com.github.cstettler.dddttc.accounting.application.WalletService; 4 | import com.github.cstettler.dddttc.accounting.domain.Wallet; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.servlet.ModelAndView; 9 | 10 | import java.math.BigDecimal; 11 | import java.util.List; 12 | 13 | import static com.github.cstettler.dddttc.accounting.infrastructure.web.AccountingController.WalletViewModel.toWalletViewModels; 14 | import static com.github.cstettler.dddttc.support.infrastructure.web.ModelAndViewBuilder.modelAndView; 15 | import static java.util.Comparator.comparing; 16 | import static java.util.stream.Collectors.toList; 17 | 18 | @Controller 19 | @RequestMapping("/accounting") 20 | class AccountingController { 21 | 22 | private final WalletService walletService; 23 | 24 | AccountingController(WalletService walletService) { 25 | this.walletService = walletService; 26 | } 27 | 28 | @GetMapping("/wallets") 29 | ModelAndView listWallets() { 30 | List wallets = this.walletService.listWallets().stream() 31 | .sorted(comparing((wallet) -> wallet.walletOwner().value())) 32 | .collect(toList()); 33 | 34 | return modelAndView("list-wallets") 35 | .property("wallets", toWalletViewModels(wallets)) 36 | .build(); 37 | } 38 | 39 | 40 | static class WalletViewModel { 41 | 42 | public final String id; 43 | public final BigDecimal balance; 44 | 45 | private WalletViewModel(Wallet wallet) { 46 | this.id = wallet.walletOwner().value(); 47 | this.balance = wallet.balance().value(); 48 | } 49 | 50 | static WalletViewModel toWalletViewModel(Wallet wallet) { 51 | return new WalletViewModel(wallet); 52 | } 53 | 54 | static List toWalletViewModels(List wallets) { 55 | return wallets.stream() 56 | .map((wallet) -> toWalletViewModel(wallet)) 57 | .collect(toList()); 58 | } 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/infrastructure/persistence/JdbcUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.infrastructure.persistence; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.user.User; 4 | import com.github.cstettler.dddttc.rental.domain.user.UserAlreadyExistsException; 5 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 6 | import com.github.cstettler.dddttc.rental.domain.user.UserNotExistingException; 7 | import com.github.cstettler.dddttc.rental.domain.user.UserRepository; 8 | import com.github.cstettler.dddttc.support.infrastructure.persistence.JsonBasedPersistenceSupport; 9 | import org.springframework.dao.DuplicateKeyException; 10 | import org.springframework.dao.EmptyResultDataAccessException; 11 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static com.github.cstettler.dddttc.rental.domain.user.UserAlreadyExistsException.userAlreadyExists; 17 | import static com.github.cstettler.dddttc.rental.domain.user.UserNotExistingException.userNotExisting; 18 | import static java.util.Collections.singletonMap; 19 | 20 | class JdbcUserRepository extends JsonBasedPersistenceSupport implements UserRepository { 21 | 22 | JdbcUserRepository(NamedParameterJdbcTemplate jdbcTemplate) { 23 | super(jdbcTemplate); 24 | } 25 | 26 | @Override 27 | public void add(User user) throws UserAlreadyExistsException { 28 | try { 29 | Map params = new HashMap<>(); 30 | params.put("id", user.id().value()); 31 | params.put("data", serializeToJson(user)); 32 | 33 | jdbcTemplate().update("INSERT INTO user (id, data) VALUES (:id, :data)", params); 34 | } catch (DuplicateKeyException e) { 35 | throw userAlreadyExists(user.id()); 36 | } 37 | } 38 | 39 | @Override 40 | public User get(UserId userId) throws UserNotExistingException { 41 | try { 42 | return jdbcTemplate().queryForObject( 43 | "SELECT data FROM user WHERE id = :id", 44 | singletonMap("id", userId.value()), 45 | deserializeFromJson(User.class) 46 | ); 47 | } catch (EmptyResultDataAccessException e) { 48 | throw userNotExisting(userId); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/infrastructure/event/BookingCompletedMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.accounting.application.WalletService; 4 | import com.github.cstettler.dddttc.accounting.domain.Booking; 5 | import com.github.cstettler.dddttc.accounting.domain.BookingAlreadyBilledException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.jms.annotation.JmsListener; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | import static com.github.cstettler.dddttc.accounting.domain.Booking.booking; 14 | import static com.github.cstettler.dddttc.accounting.domain.WalletOwner.walletOwner; 15 | 16 | @Component 17 | class BookingCompletedMessageListener { 18 | 19 | private static Logger LOGGER = LoggerFactory.getLogger(BookingCompletedMessageListener.class); 20 | 21 | private final WalletService walletService; 22 | 23 | BookingCompletedMessageListener(WalletService walletService) { 24 | this.walletService = walletService; 25 | } 26 | 27 | @JmsListener(destination = "rental/booking-completed") 28 | public void onUserRegistrationCompleted(BookingCompletedMessage message) { 29 | String userIdValue = message.userId.value; 30 | LocalDateTime startedAt = message.bikeUsage.startedAt; 31 | long durationInSeconds = message.bikeUsage.durationInSeconds; 32 | 33 | LOGGER.info("received booking completed message for user '" + userIdValue + "'"); 34 | 35 | try { 36 | Booking booking = booking(walletOwner(userIdValue), startedAt, durationInSeconds); 37 | 38 | this.walletService.billBookingFee(booking); 39 | 40 | LOGGER.info("booked rental fee for usage of '" + booking.durationInSeconds() + "' seconds to wallet of owner '" + userIdValue + "'"); 41 | } catch (BookingAlreadyBilledException e) { 42 | // ignored 43 | } 44 | } 45 | 46 | 47 | public static class BookingCompletedMessage { 48 | 49 | UserId userId; 50 | BikeUsage bikeUsage; 51 | } 52 | 53 | 54 | public static class UserId { 55 | 56 | String value; 57 | } 58 | 59 | 60 | public static class BikeUsage { 61 | 62 | LocalDateTime startedAt; 63 | long durationInSeconds; 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventHandlerAnnotationBeanPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | import com.github.cstettler.dddttc.stereotype.DomainEventHandler; 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.beans.factory.config.BeanPostProcessor; 7 | 8 | import java.lang.reflect.Method; 9 | 10 | import static java.util.Arrays.stream; 11 | import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; 12 | 13 | class DomainEventHandlerAnnotationBeanPostProcessor implements BeanPostProcessor { 14 | 15 | private final DomainEventHandlerRegistrar domainEventHandlerRegistrar; 16 | 17 | DomainEventHandlerAnnotationBeanPostProcessor(DomainEventHandlerRegistrar domainEventHandlerRegistrar) { 18 | this.domainEventHandlerRegistrar = domainEventHandlerRegistrar; 19 | } 20 | 21 | @Override 22 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 23 | stream(bean.getClass().getDeclaredMethods()) 24 | .filter((method) -> method.isAnnotationPresent(DomainEventHandler.class)) 25 | .peek((method) -> validateDomainEventHandlerSignature(method)) 26 | .forEach((domainEventHandlerMethod) -> this.domainEventHandlerRegistrar.registerDomainEventHandler(bean, domainEventHandlerMethod)); 27 | 28 | return bean; 29 | } 30 | 31 | private void validateDomainEventHandlerSignature(Method method) { 32 | if (!(method.getReturnType().equals(Void.TYPE))) { 33 | throw new IllegalStateException("domain event handler method '" + method + "' must be void"); 34 | } 35 | 36 | if (method.getParameterCount() != 1) { 37 | throw new IllegalStateException("domain event handler method '" + method + "' must declare one single domain event parameter"); 38 | } 39 | 40 | if (domainEventAnnotation(method.getParameterTypes()[0]) == null) { 41 | throw new IllegalStateException("parameter of domain handler method '" + method + "' is not annotated with '@DomainEvent'"); 42 | } 43 | } 44 | 45 | private static DomainEvent domainEventAnnotation(Class parameterType) { 46 | return findAnnotation(parameterType, DomainEvent.class); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/resources/templates/complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Registration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 |

Complete User Registration

18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-aspect/src/main/java/com/github/cstettler/dddttc/aspect/StateBasedEqualsAndHashCodeAspectAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.aspect; 2 | 3 | import com.github.cstettler.dddttc.stereotype.ValueObject; 4 | import com.google.auto.service.AutoService; 5 | 6 | import javax.annotation.processing.Processor; 7 | import javax.lang.model.element.TypeElement; 8 | import java.io.PrintWriter; 9 | import java.lang.annotation.Annotation; 10 | import java.util.Set; 11 | 12 | import static java.util.Collections.singleton; 13 | 14 | @AutoService(Processor.class) 15 | public class StateBasedEqualsAndHashCodeAspectAnnotationProcessor extends StereotypeBasedAspectAnnotationProcessor { 16 | 17 | @Override 18 | protected String aspectDescription() { 19 | return "state-based equals / hash code"; 20 | } 21 | 22 | @Override 23 | protected Set> supportedAnnotations() { 24 | return singleton(ValueObject.class); 25 | } 26 | 27 | @Override 28 | protected String aspectName(String simpleName) { 29 | return simpleName + "StateBasedEqualsAndHashCodeAspect"; 30 | } 31 | 32 | @Override 33 | protected boolean shouldWriteAspectFor(TypeElement annotatedElement) { 34 | return !hasDeclaredMethod(annotatedElement, "equals", Object.class); 35 | } 36 | 37 | @Override 38 | protected void writeAspectCode(PrintWriter writer, String aspectName, TypeElement typeElement) { 39 | writer.println("package " + elementPackage(typeElement) + ";"); 40 | writer.println(""); 41 | writer.println("import " + fullyQualifiedElementName(typeElement) + ";"); 42 | writer.println("import com.github.cstettler.dddttc.aspect.StateBasedEqualsAndHashCode;"); 43 | writer.println(""); 44 | writer.println("public aspect " + aspectName + " {"); 45 | writer.println(""); 46 | writer.println(" public boolean " + simpleName(typeElement) + ".equals(Object other) {"); 47 | writer.println(" return StateBasedEqualsAndHashCode.stateBasedEquals(this, other);"); 48 | writer.println(" }"); 49 | writer.println(""); 50 | writer.println(" public int " + simpleName(typeElement) + ".hashCode() {"); 51 | writer.println(" return StateBasedEqualsAndHashCode.stateBasedHashCode(this);"); 52 | writer.println(" }"); 53 | writer.println(""); 54 | writer.println("}"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/PendingDomainEventStore.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import org.springframework.jdbc.core.RowMapper; 4 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import static java.util.Collections.singletonMap; 12 | 13 | class PendingDomainEventStore { 14 | 15 | private final NamedParameterJdbcTemplate jdbcTemplate; 16 | 17 | PendingDomainEventStore(NamedParameterJdbcTemplate jdbcTemplate) { 18 | this.jdbcTemplate = jdbcTemplate; 19 | } 20 | 21 | void storePendingDomainEvent(PendingDomainEvent pendingDomainEvent) { 22 | Map params = new HashMap<>(); 23 | params.put("id", pendingDomainEvent.id()); 24 | params.put("type", pendingDomainEvent.type()); 25 | params.put("payload", pendingDomainEvent.payload()); 26 | params.put("publishedAt", pendingDomainEvent.publishedAt()); 27 | 28 | this.jdbcTemplate.update( 29 | "INSERT INTO domain_event (id, type, payload, published_at) VALUES(:id, :type, :payload, :publishedAt)", 30 | params 31 | ); 32 | } 33 | 34 | List loadNextPendingDomainEvents(int count) { 35 | return this.jdbcTemplate.query( 36 | "SELECT id, type, payload, published_at FROM domain_event WHERE dispatched = 0 ORDER BY published_at ASC LIMIT :count", 37 | singletonMap("count", count), 38 | pendingDomainEventRowMapper() 39 | ); 40 | } 41 | 42 | void markPendingDomainEventDispatched(PendingDomainEvent pendingDomainEvent) { 43 | this.jdbcTemplate.update( 44 | "UPDATE domain_event SET dispatched = 1 WHERE id = :id", 45 | singletonMap("id", pendingDomainEvent.id()) 46 | ); 47 | } 48 | 49 | private RowMapper pendingDomainEventRowMapper() { 50 | return (resultSet, i) -> { 51 | String id = resultSet.getString("id"); 52 | String type = resultSet.getString("type"); 53 | String payload = resultSet.getString("payload"); 54 | LocalDateTime publishedAt = resultSet.getTimestamp("published_at").toLocalDateTime(); 55 | 56 | return new PendingDomainEvent(id, type, payload, publishedAt); 57 | }; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventSupportConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import org.springframework.beans.factory.config.BeanPostProcessor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | import org.springframework.transaction.PlatformTransactionManager; 8 | 9 | import javax.jms.ConnectionFactory; 10 | import java.util.List; 11 | 12 | import static com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings.domainEventTypeMappings; 13 | 14 | @EnableScheduling 15 | public class DomainEventSupportConfiguration { 16 | 17 | @Bean 18 | PendingDomainEventStore pendingDomainEventStore(NamedParameterJdbcTemplate jdbcTemplate) { 19 | return new PendingDomainEventStore(jdbcTemplate); 20 | } 21 | 22 | @Bean 23 | PendingDomainEventPublisher pendingDomainEventDispatcher(PendingDomainEventStore pendingDomainEventStore, PlatformTransactionManager transactionManager, ConnectionFactory connectionFactory) { 24 | return new PendingDomainEventPublisher(pendingDomainEventStore, transactionManager, connectionFactory); 25 | } 26 | 27 | @Bean(destroyMethod = "stopMessageListenerContainers") 28 | DomainEventHandlerRegistrar domainEventHandlerRegistrar(DomainEventTypeResolver domainEventTypeResolver, DomainEventSerializer domainEventSerializer, ConnectionFactory connectionFactory, PlatformTransactionManager transactionManager) { 29 | return new DomainEventHandlerRegistrar(domainEventTypeResolver, domainEventSerializer, connectionFactory, transactionManager); 30 | } 31 | 32 | @Bean 33 | DomainEventTypeResolver domainEventTypeResolver(List domainEventTypeMappings) { 34 | return new DomainEventTypeResolver(domainEventTypeMappings); 35 | } 36 | 37 | @Bean 38 | DomainEventSerializer domainEventSerializer() { 39 | return new DomainEventSerializer(); 40 | } 41 | 42 | @Bean 43 | DomainEventTypeMappings defaultDomainEventTypeMappings() { 44 | return domainEventTypeMappings().build(); 45 | } 46 | 47 | @Bean 48 | BeanPostProcessor domainEventHandlerAnnotationBeanPostProcessor(DomainEventHandlerRegistrar domainEventHandlerRegistrar) { 49 | return new DomainEventHandlerAnnotationBeanPostProcessor(domainEventHandlerRegistrar); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/AccountingApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting; 2 | 3 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 4 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 5 | import com.github.cstettler.dddttc.stereotype.DomainService; 6 | import com.github.cstettler.dddttc.support.InfrastructureServiceImplementationFilter; 7 | import com.github.cstettler.dddttc.support.RepositoryImplementationFilter; 8 | import com.github.cstettler.dddttc.support.infrastructure.event.DomainEventSupportConfiguration; 9 | import com.github.cstettler.dddttc.support.infrastructure.event.JmsListenerConfiguration; 10 | import com.github.cstettler.dddttc.support.infrastructure.persistence.TransactionConfiguration; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; 13 | import org.springframework.boot.autoconfigure.SpringBootApplication; 14 | import org.springframework.boot.context.TypeExcludeFilter; 15 | import org.springframework.context.annotation.ComponentScan; 16 | import org.springframework.context.annotation.ComponentScan.Filter; 17 | import org.springframework.context.annotation.Import; 18 | 19 | import static org.springframework.context.annotation.FilterType.ANNOTATION; 20 | import static org.springframework.context.annotation.FilterType.CUSTOM; 21 | 22 | @SpringBootApplication 23 | @ComponentScan( 24 | basePackages = { 25 | "com.github.cstettler.dddttc.accounting", 26 | "com.github.cstettler.dddttc.support" 27 | }, 28 | includeFilters = { 29 | @Filter(type = ANNOTATION, classes = ApplicationService.class), 30 | @Filter(type = ANNOTATION, classes = DomainService.class), 31 | @Filter(type = ANNOTATION, classes = AggregateFactory.class), 32 | @Filter(type = CUSTOM, classes = RepositoryImplementationFilter.class), 33 | @Filter(type = CUSTOM, classes = InfrastructureServiceImplementationFilter.class) 34 | }, 35 | excludeFilters = { 36 | @Filter(type = CUSTOM, classes = TypeExcludeFilter.class), 37 | @Filter(type = CUSTOM, classes = AutoConfigurationExcludeFilter.class) 38 | } 39 | ) 40 | @Import({ 41 | DomainEventSupportConfiguration.class, 42 | TransactionConfiguration.class, 43 | JmsListenerConfiguration.class 44 | }) 45 | public class AccountingApplication { 46 | 47 | public static void main(String[] args) { 48 | SpringApplication.run(AccountingApplication.class, args); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/TransactionalEnqueuingDomainEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.stereotype.DomainEvent; 4 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | import static java.time.LocalDateTime.now; 11 | import static java.util.UUID.randomUUID; 12 | import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; 13 | import static org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive; 14 | 15 | class TransactionalEnqueuingDomainEventPublisher implements DomainEventPublisher { 16 | 17 | private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalEnqueuingDomainEventPublisher.class); 18 | 19 | private final PendingDomainEventStore pendingDomainEventStore; 20 | private final DomainEventTypeResolver domainEventTypeResolver; 21 | private final DomainEventSerializer domainEventSerializer; 22 | 23 | TransactionalEnqueuingDomainEventPublisher(PendingDomainEventStore pendingDomainEventStore, DomainEventTypeResolver domainEventTypeResolver, DomainEventSerializer domainEventSerializer) { 24 | this.pendingDomainEventStore = pendingDomainEventStore; 25 | this.domainEventTypeResolver = domainEventTypeResolver; 26 | this.domainEventSerializer = domainEventSerializer; 27 | } 28 | 29 | @Override 30 | public void publish(Object domainEvent) { 31 | if (domainEventAnnotation(domainEvent.getClass()) == null) { 32 | throw new IllegalStateException("published domain event '" + domainEvent + "' is not annotated with @DomainEvent"); 33 | } 34 | 35 | if (!(isActualTransactionActive())) { 36 | throw new IllegalStateException("no transaction active when trying to enqueue domain event '" + domainEvent + "'"); 37 | } 38 | 39 | String id = randomUUID().toString(); 40 | String type = this.domainEventTypeResolver.resolveDomainEventType(domainEvent.getClass()); 41 | String payload = this.domainEventSerializer.serialize(domainEvent); 42 | LocalDateTime publishedAt = now(); 43 | 44 | PendingDomainEvent pendingDomainEvent = new PendingDomainEvent(id, type, payload, publishedAt); 45 | this.pendingDomainEventStore.storePendingDomainEvent(pendingDomainEvent); 46 | 47 | LOGGER.info("enqueued domain event '" + id + "' for '" + type + "' (payload '" + payload + "'"); 48 | } 49 | 50 | private static DomainEvent domainEventAnnotation(Class domainEventType) { 51 | return findAnnotation(domainEventType, DomainEvent.class); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/infrastructure/event/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 4 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 5 | import com.github.cstettler.dddttc.stereotype.DomainService; 6 | import com.github.cstettler.dddttc.support.InfrastructureServiceImplementationFilter; 7 | import com.github.cstettler.dddttc.support.RepositoryImplementationFilter; 8 | import com.github.cstettler.dddttc.support.infrastructure.persistence.TransactionConfiguration; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; 11 | import org.springframework.boot.autoconfigure.SpringBootApplication; 12 | import org.springframework.boot.context.TypeExcludeFilter; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.ComponentScan; 15 | import org.springframework.context.annotation.ComponentScan.Filter; 16 | import org.springframework.context.annotation.Import; 17 | 18 | import static com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings.domainEventTypeMappings; 19 | import static org.springframework.context.annotation.FilterType.ANNOTATION; 20 | import static org.springframework.context.annotation.FilterType.CUSTOM; 21 | 22 | @SpringBootApplication 23 | @ComponentScan( 24 | basePackages = { 25 | "com.github.cstettler.dddttc.support" 26 | }, 27 | useDefaultFilters = false, 28 | includeFilters = { 29 | @Filter(type = ANNOTATION, classes = ApplicationService.class), 30 | @Filter(type = ANNOTATION, classes = DomainService.class), 31 | @Filter(type = ANNOTATION, classes = AggregateFactory.class), 32 | @Filter(type = CUSTOM, classes = RepositoryImplementationFilter.class), 33 | @Filter(type = CUSTOM, classes = InfrastructureServiceImplementationFilter.class) 34 | }, 35 | excludeFilters = { 36 | @Filter(type = CUSTOM, classes = TypeExcludeFilter.class), 37 | @Filter(type = CUSTOM, classes = AutoConfigurationExcludeFilter.class) 38 | } 39 | ) 40 | @Import({ 41 | DomainEventSupportConfiguration.class, 42 | TransactionConfiguration.class, 43 | }) 44 | class TestApplication { 45 | 46 | public static void main(String[] args) { 47 | SpringApplication.run(TestApplication.class, args); 48 | } 49 | 50 | @Bean 51 | DomainEventTypeMappings testDomainEventTypeMappings() { 52 | return domainEventTypeMappings() 53 | .addMapping(TestDomainEvent.class, "support/test-domain-event") 54 | .build(); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/domain/booking/Booking.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.domain.booking; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.bike.NumberPlate; 4 | import com.github.cstettler.dddttc.rental.domain.user.UserId; 5 | import com.github.cstettler.dddttc.stereotype.Aggregate; 6 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 7 | import com.github.cstettler.dddttc.stereotype.AggregateId; 8 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 9 | 10 | import java.time.Clock; 11 | import java.time.LocalDateTime; 12 | 13 | import static com.github.cstettler.dddttc.rental.domain.booking.BikeUsage.bikeUsage; 14 | import static com.github.cstettler.dddttc.rental.domain.booking.BookingAlreadyCompletedException.bookingAlreadyCompleted; 15 | import static com.github.cstettler.dddttc.rental.domain.booking.BookingCompletedEvent.bookingCompleted; 16 | import static com.github.cstettler.dddttc.rental.domain.booking.BookingId.newBookingId; 17 | import static java.time.LocalDateTime.now; 18 | 19 | @Aggregate 20 | public class Booking { 21 | 22 | private final BookingId id; 23 | private final NumberPlate numberPlate; 24 | private final UserId userId; 25 | private final LocalDateTime bikeBookedAt; 26 | private LocalDateTime bikeReturnedAt; 27 | private boolean completed; 28 | private transient final DomainEventPublisher domainEventPublisher; 29 | 30 | private Booking(NumberPlate numberPlate, UserId userId, DomainEventPublisher domainEventPublisher, Clock clock) { 31 | this.numberPlate = numberPlate; 32 | this.userId = userId; 33 | this.domainEventPublisher = domainEventPublisher; 34 | 35 | this.id = newBookingId(); 36 | this.bikeBookedAt = now(clock); 37 | this.bikeReturnedAt = null; 38 | this.completed = false; 39 | } 40 | 41 | @AggregateId 42 | public BookingId id() { 43 | return this.id; 44 | } 45 | 46 | public NumberPlate numberPlate() { 47 | return this.numberPlate; 48 | } 49 | 50 | public UserId userId() { 51 | return this.userId; 52 | } 53 | 54 | public LocalDateTime bikeBookedAt() { 55 | return this.bikeBookedAt; 56 | } 57 | 58 | public LocalDateTime bikeReturnedAt() { 59 | return this.bikeReturnedAt; 60 | } 61 | 62 | public boolean completed() { 63 | return this.completed; 64 | } 65 | 66 | public void returnBike(Clock clock) { 67 | if (this.completed) { 68 | throw bookingAlreadyCompleted(this.id); 69 | } 70 | 71 | this.bikeReturnedAt = now(clock); 72 | this.completed = true; 73 | 74 | BikeUsage bikeUsage = bikeUsage(this.bikeBookedAt, this.bikeReturnedAt); 75 | this.domainEventPublisher.publish(bookingCompleted(this.id, this.numberPlate, this.userId, bikeUsage)); 76 | } 77 | 78 | @AggregateFactory(Booking.class) 79 | public static Booking newBooking(NumberPlate numberPlate, UserId userId, DomainEventPublisher domainEventPublisher, Clock clock) { 80 | return new Booking(numberPlate, userId, domainEventPublisher, clock); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/infrastructure/persistence/JdbcBookingRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental.infrastructure.persistence; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.booking.Booking; 4 | import com.github.cstettler.dddttc.rental.domain.booking.BookingId; 5 | import com.github.cstettler.dddttc.rental.domain.booking.BookingNotExistingException; 6 | import com.github.cstettler.dddttc.rental.domain.booking.BookingRepository; 7 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 8 | import com.github.cstettler.dddttc.support.infrastructure.persistence.JsonBasedPersistenceSupport; 9 | import org.springframework.dao.EmptyResultDataAccessException; 10 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 11 | 12 | import java.util.Collection; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static com.github.cstettler.dddttc.rental.domain.booking.BookingNotExistingException.bookingNotExisting; 17 | import static java.util.Collections.emptyMap; 18 | import static java.util.Collections.singletonMap; 19 | 20 | class JdbcBookingRepository extends JsonBasedPersistenceSupport implements BookingRepository { 21 | 22 | private final DomainEventPublisher domainEventPublisher; 23 | 24 | JdbcBookingRepository(NamedParameterJdbcTemplate jdbcTemplate, DomainEventPublisher domainEventPublisher) { 25 | super(jdbcTemplate); 26 | 27 | this.domainEventPublisher = domainEventPublisher; 28 | } 29 | 30 | @Override 31 | public void add(Booking booking) { 32 | Map params = new HashMap<>(); 33 | params.put("id", booking.id().value()); 34 | params.put("data", serializeToJson(booking)); 35 | 36 | jdbcTemplate().update("INSERT INTO booking (id, data) VALUES (:id, :data)", params); 37 | } 38 | 39 | @Override 40 | public void update(Booking booking) { 41 | jdbcTemplate().update("DELETE FROM booking WHERE id = :id", singletonMap("id", booking.id().value())); 42 | 43 | Map params = new HashMap<>(); 44 | params.put("id", booking.id().value()); 45 | params.put("data", serializeToJson(booking)); 46 | 47 | jdbcTemplate().update("INSERT INTO booking (id, data) VALUES (:id, :data)", params); 48 | } 49 | 50 | @Override 51 | public Booking get(BookingId bookingId) throws BookingNotExistingException { 52 | try { 53 | return inject(jdbcTemplate().queryForObject( 54 | "SELECT data FROM booking WHERE id = :id", 55 | singletonMap("id", bookingId.value()), 56 | deserializeFromJson(Booking.class) 57 | ), this.domainEventPublisher); 58 | } catch (EmptyResultDataAccessException e) { 59 | throw bookingNotExisting(bookingId); 60 | } 61 | } 62 | 63 | @Override 64 | public Collection findAll() { 65 | return inject(jdbcTemplate().query( 66 | "SELECT data FROM booking", 67 | emptyMap(), 68 | deserializeFromJson(Booking.class) 69 | ), this.domainEventPublisher); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/java/com/github/cstettler/dddttc/accounting/infrastructure/test/UserRegistrationCompletedMessageListenerTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.test; 2 | 3 | import com.github.cstettler.dddttc.accounting.application.WalletService; 4 | import com.github.cstettler.dddttc.support.test.ScenarioTest; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.mock.mockito.MockBean; 8 | import org.springframework.jms.core.JmsTemplate; 9 | 10 | import javax.jms.ConnectionFactory; 11 | 12 | import static com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException.walletAlreadyExists; 13 | import static com.github.cstettler.dddttc.accounting.domain.WalletOwner.walletOwner; 14 | import static java.util.concurrent.TimeUnit.SECONDS; 15 | import static org.awaitility.Awaitility.await; 16 | import static org.mockito.Mockito.doThrow; 17 | import static org.mockito.Mockito.verify; 18 | 19 | 20 | @ScenarioTest 21 | class UserRegistrationCompletedMessageListenerTests { 22 | 23 | @MockBean 24 | private WalletService walletService; 25 | 26 | @Autowired 27 | private ConnectionFactory connectionFactory; 28 | 29 | @Test 30 | void initializeWalletOnUserRegistrationCompletedEventDelivery() { 31 | // arrange 32 | String domainEventType = "registration/user-registration-completed"; 33 | String domainEventPayload = asJson("" + 34 | "{" + 35 | " 'userHandle': {" + 36 | " 'value': 'peter'" + 37 | " }" + 38 | "}" 39 | ); 40 | 41 | // act 42 | dispatchMessage(domainEventType, domainEventPayload); 43 | 44 | // assert 45 | await().atMost(3, SECONDS).untilAsserted(() -> verify(this.walletService).initializeWallet(walletOwner("peter"))); 46 | } 47 | 48 | @Test 49 | void initializeWalletOnUserRegistrationCompletedEventRedelivery() { 50 | // arrange 51 | String domainEventType = "registration/user-registration-completed"; 52 | String domainEventPayload = asJson("" + 53 | "{" + 54 | " 'userHandle': {" + 55 | " 'value': 'peter'" + 56 | " }" + 57 | "}" 58 | ); 59 | 60 | doThrow(walletAlreadyExists(walletOwner("peter"))).when(this.walletService).initializeWallet(walletOwner("peter")); 61 | 62 | // act 63 | dispatchMessage(domainEventType, domainEventPayload); 64 | 65 | // assert 66 | await().atMost(3, SECONDS).untilAsserted(() -> verify(this.walletService).initializeWallet(walletOwner("peter"))); 67 | } 68 | 69 | private void dispatchMessage(String destination, String domainEventPayload) { 70 | JmsTemplate jmsTemplate = new JmsTemplate(); 71 | jmsTemplate.setConnectionFactory(this.connectionFactory); 72 | jmsTemplate.setPubSubDomain(true); 73 | jmsTemplate.send(destination, (session) -> session.createTextMessage(domainEventPayload)); 74 | } 75 | 76 | private static String asJson(String value) { 77 | return value.replace('\'', '"'); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/infrastructure/event/DomainEventSupportTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import com.github.cstettler.dddttc.support.EnableComponentScanExclusions; 4 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.junit.jupiter.api.function.Executable; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.test.context.ActiveProfiles; 13 | import org.springframework.test.context.junit.jupiter.SpringExtension; 14 | import org.springframework.transaction.PlatformTransactionManager; 15 | import org.springframework.transaction.TransactionStatus; 16 | import org.springframework.transaction.support.TransactionCallbackWithoutResult; 17 | import org.springframework.transaction.support.TransactionTemplate; 18 | 19 | import static com.github.cstettler.dddttc.support.infrastructure.event.TestDomainEvent.testDomainEvent; 20 | import static java.util.concurrent.TimeUnit.SECONDS; 21 | import static org.awaitility.Awaitility.await; 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.hasSize; 24 | import static org.hamcrest.Matchers.is; 25 | 26 | @ExtendWith(SpringExtension.class) 27 | @SpringBootTest 28 | @AutoConfigureTestDatabase 29 | @EnableComponentScanExclusions 30 | @ActiveProfiles("test") 31 | class DomainEventSupportTests { 32 | 33 | @Autowired 34 | private DomainEventPublisher domainEventPublisher; 35 | 36 | @Autowired 37 | private TestDomainService testDomainService; 38 | 39 | @Autowired 40 | private PlatformTransactionManager transactionManager; 41 | 42 | @AfterEach 43 | void clearRecordedDomainEvents() { 44 | this.testDomainService.clearRecordedDomainEvents(); 45 | } 46 | 47 | @Test 48 | void publish_testDomainEventAndDomainEventHandler_dispatchesDomainEventToDomainEventHandler() { 49 | // arrange 50 | TestDomainEvent testDomainEvent = testDomainEvent("test-value"); 51 | 52 | // act 53 | executeInTransaction(() -> this.domainEventPublisher.publish(testDomainEvent)); 54 | 55 | // assert 56 | await().atMost(3, SECONDS).until(() -> this.testDomainService.recordedDomainEvents().size() > 0); 57 | 58 | assertThat(this.testDomainService.recordedDomainEvents(), hasSize(1)); 59 | assertThat(this.testDomainService.recordedDomainEvents().get(0).value(), is("test-value")); 60 | } 61 | 62 | private void executeInTransaction(Executable executable) { 63 | new TransactionTemplate(this.transactionManager).execute(new TransactionCallbackWithoutResult() { 64 | @Override 65 | protected void doInTransactionWithoutResult(TransactionStatus status) { 66 | try { 67 | executable.execute(); 68 | } catch (Throwable t) { 69 | throw new IllegalStateException("execution in transaction failed", t); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/infrastructure/persistence/JdbcWalletRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.persistence; 2 | 3 | import com.github.cstettler.dddttc.accounting.domain.Wallet; 4 | import com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException; 5 | import com.github.cstettler.dddttc.accounting.domain.WalletNotExistingException; 6 | import com.github.cstettler.dddttc.accounting.domain.WalletOwner; 7 | import com.github.cstettler.dddttc.accounting.domain.WalletRepository; 8 | import com.github.cstettler.dddttc.support.infrastructure.persistence.JsonBasedPersistenceSupport; 9 | import org.springframework.dao.DuplicateKeyException; 10 | import org.springframework.dao.EmptyResultDataAccessException; 11 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 12 | 13 | import java.util.Collection; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException.walletAlreadyExists; 18 | import static com.github.cstettler.dddttc.accounting.domain.WalletNotExistingException.walletNotExisting; 19 | import static java.util.Collections.emptyMap; 20 | import static java.util.Collections.singletonMap; 21 | 22 | class JdbcWalletRepository extends JsonBasedPersistenceSupport implements WalletRepository { 23 | 24 | JdbcWalletRepository(NamedParameterJdbcTemplate jdbcTemplate) { 25 | super(jdbcTemplate); 26 | } 27 | 28 | @Override 29 | public void add(Wallet wallet) throws WalletAlreadyExistsException { 30 | try { 31 | Map params = new HashMap<>(); 32 | params.put("id", wallet.walletOwner().value()); 33 | params.put("data", serializeToJson(wallet)); 34 | 35 | jdbcTemplate().update("INSERT INTO wallet (id, data) VALUES (:id, :data)", params); 36 | } catch (DuplicateKeyException e) { 37 | throw walletAlreadyExists(wallet.walletOwner()); 38 | } 39 | } 40 | 41 | @Override 42 | public void update(Wallet wallet) throws WalletNotExistingException { 43 | jdbcTemplate().update("DELETE FROM wallet WHERE id = :id", singletonMap("id", wallet.walletOwner().value())); 44 | 45 | Map params = new HashMap<>(); 46 | params.put("id", wallet.walletOwner().value()); 47 | params.put("data", serializeToJson(wallet)); 48 | 49 | jdbcTemplate().update("INSERT INTO wallet (id, data) VALUES (:id, :data)", params); 50 | } 51 | 52 | @Override 53 | public Wallet get(WalletOwner walletOwner) throws WalletNotExistingException { 54 | try { 55 | return jdbcTemplate().queryForObject( 56 | "SELECT data FROM wallet WHERE id = :id", 57 | singletonMap("id", walletOwner.value()), 58 | deserializeFromJson(Wallet.class) 59 | ); 60 | } catch (EmptyResultDataAccessException e) { 61 | throw walletNotExisting(walletOwner); 62 | } 63 | } 64 | 65 | @Override 66 | public Collection findAll() { 67 | return jdbcTemplate().query( 68 | "SELECT data FROM wallet", 69 | emptyMap(), 70 | deserializeFromJson(Wallet.class) 71 | ); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-rental/src/main/java/com/github/cstettler/dddttc/rental/RentalApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.rental; 2 | 3 | import com.github.cstettler.dddttc.rental.domain.booking.BookingCompletedEvent; 4 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 5 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 6 | import com.github.cstettler.dddttc.stereotype.DomainService; 7 | import com.github.cstettler.dddttc.support.InfrastructureServiceImplementationFilter; 8 | import com.github.cstettler.dddttc.support.RepositoryImplementationFilter; 9 | import com.github.cstettler.dddttc.support.infrastructure.event.DomainEventSupportConfiguration; 10 | import com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings; 11 | import com.github.cstettler.dddttc.support.infrastructure.event.JmsListenerConfiguration; 12 | import com.github.cstettler.dddttc.support.infrastructure.persistence.TransactionConfiguration; 13 | import org.springframework.boot.SpringApplication; 14 | import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; 15 | import org.springframework.boot.autoconfigure.SpringBootApplication; 16 | import org.springframework.boot.context.TypeExcludeFilter; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.ComponentScan; 19 | import org.springframework.context.annotation.Import; 20 | 21 | import java.time.Clock; 22 | 23 | import static com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings.domainEventTypeMappings; 24 | import static org.springframework.context.annotation.FilterType.ANNOTATION; 25 | import static org.springframework.context.annotation.FilterType.CUSTOM; 26 | 27 | @SpringBootApplication 28 | @ComponentScan( 29 | basePackages = { 30 | "com.github.cstettler.dddttc.rental", 31 | "com.github.cstettler.dddttc.support" 32 | }, 33 | includeFilters = { 34 | @ComponentScan.Filter(type = ANNOTATION, classes = ApplicationService.class), 35 | @ComponentScan.Filter(type = ANNOTATION, classes = DomainService.class), 36 | @ComponentScan.Filter(type = ANNOTATION, classes = AggregateFactory.class), 37 | @ComponentScan.Filter(type = CUSTOM, classes = RepositoryImplementationFilter.class), 38 | @ComponentScan.Filter(type = CUSTOM, classes = InfrastructureServiceImplementationFilter.class) 39 | }, 40 | excludeFilters = { 41 | @ComponentScan.Filter(type = CUSTOM, classes = TypeExcludeFilter.class), 42 | @ComponentScan.Filter(type = CUSTOM, classes = AutoConfigurationExcludeFilter.class) 43 | } 44 | ) 45 | @Import({ 46 | DomainEventSupportConfiguration.class, 47 | TransactionConfiguration.class, 48 | JmsListenerConfiguration.class 49 | }) 50 | public class RentalApplication { 51 | 52 | public static void main(String[] args) { 53 | SpringApplication.run(RentalApplication.class, args); 54 | } 55 | 56 | @Bean 57 | DomainEventTypeMappings registrationDomainEventTypeMappings() { 58 | return domainEventTypeMappings() 59 | .addMapping(BookingCompletedEvent.class, "rental/booking-completed") 60 | .build(); 61 | } 62 | 63 | @Bean 64 | Clock clock() { 65 | return Clock.systemUTC(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/RegistrationApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration; 2 | 3 | import com.github.cstettler.dddttc.registration.domain.PhoneNumberVerificationCodeGeneratedEvent; 4 | import com.github.cstettler.dddttc.registration.domain.UserRegistrationCompletedEvent; 5 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 6 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 7 | import com.github.cstettler.dddttc.stereotype.DomainService; 8 | import com.github.cstettler.dddttc.support.InfrastructureServiceImplementationFilter; 9 | import com.github.cstettler.dddttc.support.RepositoryImplementationFilter; 10 | import com.github.cstettler.dddttc.support.infrastructure.event.DomainEventSupportConfiguration; 11 | import com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings; 12 | import com.github.cstettler.dddttc.support.infrastructure.persistence.TransactionConfiguration; 13 | import org.springframework.boot.SpringApplication; 14 | import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; 15 | import org.springframework.boot.autoconfigure.SpringBootApplication; 16 | import org.springframework.boot.context.TypeExcludeFilter; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.ComponentScan; 19 | import org.springframework.context.annotation.ComponentScan.Filter; 20 | import org.springframework.context.annotation.Import; 21 | 22 | import static com.github.cstettler.dddttc.support.infrastructure.event.DomainEventTypeMappings.domainEventTypeMappings; 23 | import static org.springframework.context.annotation.FilterType.ANNOTATION; 24 | import static org.springframework.context.annotation.FilterType.CUSTOM; 25 | 26 | @SpringBootApplication 27 | @ComponentScan( 28 | basePackages = { 29 | "com.github.cstettler.dddttc.registration", 30 | "com.github.cstettler.dddttc.support" 31 | }, 32 | includeFilters = { 33 | @Filter(type = ANNOTATION, classes = ApplicationService.class), 34 | @Filter(type = ANNOTATION, classes = DomainService.class), 35 | @Filter(type = ANNOTATION, classes = AggregateFactory.class), 36 | @Filter(type = CUSTOM, classes = RepositoryImplementationFilter.class), 37 | @Filter(type = CUSTOM, classes = InfrastructureServiceImplementationFilter.class) 38 | }, 39 | excludeFilters = { 40 | @Filter(type = CUSTOM, classes = TypeExcludeFilter.class), 41 | @Filter(type = CUSTOM, classes = AutoConfigurationExcludeFilter.class) 42 | } 43 | ) 44 | @Import({ 45 | DomainEventSupportConfiguration.class, 46 | TransactionConfiguration.class, 47 | }) 48 | public class RegistrationApplication { 49 | 50 | public static void main(String[] args) { 51 | SpringApplication.run(RegistrationApplication.class, args); 52 | } 53 | 54 | @Bean 55 | DomainEventTypeMappings registrationDomainEventTypeMappings() { 56 | return domainEventTypeMappings() 57 | .addMapping(PhoneNumberVerificationCodeGeneratedEvent.class, "registration/phone-number-verification-code-generated") 58 | .addMapping(UserRegistrationCompletedEvent.class, "registration/user-registration-completed") 59 | .build(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-registration/src/main/java/com/github/cstettler/dddttc/registration/application/UserRegistrationService.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.registration.application; 2 | 3 | import com.github.cstettler.dddttc.registration.domain.FullName; 4 | import com.github.cstettler.dddttc.registration.domain.PhoneNumber; 5 | import com.github.cstettler.dddttc.registration.domain.PhoneNumberAlreadyVerifiedException; 6 | import com.github.cstettler.dddttc.registration.domain.PhoneNumberNotSwissException; 7 | import com.github.cstettler.dddttc.registration.domain.PhoneNumberNotYetVerifiedException; 8 | import com.github.cstettler.dddttc.registration.domain.PhoneNumberVerificationCodeInvalidException; 9 | import com.github.cstettler.dddttc.registration.domain.UserHandle; 10 | import com.github.cstettler.dddttc.registration.domain.UserHandleAlreadyInUseException; 11 | import com.github.cstettler.dddttc.registration.domain.UserRegistration; 12 | import com.github.cstettler.dddttc.registration.domain.UserRegistration.UserRegistrationFactory; 13 | import com.github.cstettler.dddttc.registration.domain.UserRegistrationAlreadyCompletedException; 14 | import com.github.cstettler.dddttc.registration.domain.UserRegistrationId; 15 | import com.github.cstettler.dddttc.registration.domain.UserRegistrationNotExistingException; 16 | import com.github.cstettler.dddttc.registration.domain.UserRegistrationRepository; 17 | import com.github.cstettler.dddttc.registration.domain.VerificationCode; 18 | import com.github.cstettler.dddttc.stereotype.ApplicationService; 19 | 20 | @ApplicationService 21 | public class UserRegistrationService { 22 | 23 | private final UserRegistrationFactory userRegistrationFactory; 24 | private final UserRegistrationRepository userRegistrationRepository; 25 | 26 | UserRegistrationService(UserRegistrationFactory userRegistrationFactory, UserRegistrationRepository userRegistrationRepository) { 27 | this.userRegistrationFactory = userRegistrationFactory; 28 | this.userRegistrationRepository = userRegistrationRepository; 29 | } 30 | 31 | public UserRegistrationId startNewUserRegistrationProcess(UserHandle userHandle, PhoneNumber phoneNumber) throws UserHandleAlreadyInUseException, PhoneNumberNotSwissException { 32 | UserRegistration userRegistration = this.userRegistrationFactory.newUserRegistration(userHandle, phoneNumber); 33 | UserRegistrationId userRegistrationId = userRegistration.id(); 34 | 35 | this.userRegistrationRepository.add(userRegistration); 36 | 37 | return userRegistrationId; 38 | } 39 | 40 | public void verifyPhoneNumber(UserRegistrationId userRegistrationId, VerificationCode verificationCode) throws UserRegistrationNotExistingException, PhoneNumberVerificationCodeInvalidException, PhoneNumberAlreadyVerifiedException { 41 | UserRegistration userRegistration = this.userRegistrationRepository.get(userRegistrationId); 42 | userRegistration.verifyPhoneNumber(verificationCode); 43 | 44 | this.userRegistrationRepository.update(userRegistration); 45 | } 46 | 47 | public void completeUserRegistration(UserRegistrationId userRegistrationId, FullName fullName) throws UserRegistrationNotExistingException, PhoneNumberNotYetVerifiedException, UserRegistrationAlreadyCompletedException { 48 | UserRegistration userRegistration = this.userRegistrationRepository.get(userRegistrationId); 49 | userRegistration.complete(fullName); 50 | 51 | this.userRegistrationRepository.update(userRegistration); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/persistence/JsonBasedPersistenceSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.persistence; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 6 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 7 | import org.springframework.jdbc.core.RowMapper; 8 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 9 | 10 | import java.io.IOException; 11 | import java.lang.reflect.Field; 12 | import java.util.Collection; 13 | 14 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; 15 | import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD; 16 | import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; 17 | 18 | public abstract class JsonBasedPersistenceSupport { 19 | 20 | private final NamedParameterJdbcTemplate jdbcTemplate; 21 | private final ObjectMapper objectMapper; 22 | 23 | public JsonBasedPersistenceSupport(NamedParameterJdbcTemplate jdbcTemplate) { 24 | this.jdbcTemplate = jdbcTemplate; 25 | this.objectMapper = fieldAccessEnabledObjectMapper(); 26 | } 27 | 28 | protected NamedParameterJdbcTemplate jdbcTemplate() { 29 | return this.jdbcTemplate; 30 | } 31 | 32 | protected String serializeToJson(Object object) { 33 | try { 34 | return this.objectMapper.writeValueAsString(object); 35 | } catch (JsonProcessingException e) { 36 | throw new IllegalStateException("unable to serialize object '" + object + "' to json", e); 37 | } 38 | } 39 | 40 | protected RowMapper deserializeFromJson(Class type) { 41 | return (rs, rowNum) -> deserializeJsonValue(rs.getString("data"), type); 42 | } 43 | 44 | protected T deserializeJsonValue(String data, Class type) { 45 | try { 46 | return this.objectMapper.readValue(data, type); 47 | } catch (IOException e) { 48 | throw new IllegalStateException("unable to read json data into object of type '" + type.getName() + "'", e); 49 | } 50 | } 51 | 52 | protected static > T inject(T collectionOfTargets, DomainEventPublisher domainEventPublisher) { 53 | collectionOfTargets.forEach((target) -> inject(target, domainEventPublisher)); 54 | 55 | return collectionOfTargets; 56 | } 57 | 58 | protected static T inject(T target, DomainEventPublisher domainEventPublisher) { 59 | try { 60 | Field domainEventPublisherField = target.getClass().getDeclaredField("domainEventPublisher"); 61 | domainEventPublisherField.setAccessible(true); 62 | domainEventPublisherField.set(target, domainEventPublisher); 63 | } catch (Exception e) { 64 | throw new IllegalStateException("unable to inject domain event publisher into aggregate instance '" + target + "'", e); 65 | } 66 | 67 | return target; 68 | } 69 | 70 | private static ObjectMapper fieldAccessEnabledObjectMapper() { 71 | ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder() 72 | .featuresToDisable(WRITE_DATES_AS_TIMESTAMPS) 73 | .build(); 74 | 75 | objectMapper.setVisibility(FIELD, ANY); 76 | 77 | return objectMapper; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/test/java/com/github/cstettler/dddttc/support/ReflectionBasedStateMatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support; 2 | 3 | import org.hamcrest.Description; 4 | import org.hamcrest.Matcher; 5 | import org.hamcrest.TypeSafeDiagnosingMatcher; 6 | 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | 10 | import static com.github.cstettler.dddttc.support.ReflectionUtils.propertyValue; 11 | import static java.util.stream.Collectors.joining; 12 | 13 | public abstract class ReflectionBasedStateMatcher extends TypeSafeDiagnosingMatcher { 14 | 15 | private static final String INDENTATION = " "; 16 | 17 | private final String objectName; 18 | private final Map> matchersByPropertyName; 19 | 20 | protected ReflectionBasedStateMatcher(String objectName, Map> matchersByPropertyName) { 21 | this.objectName = objectName; 22 | this.matchersByPropertyName = matchersByPropertyName; 23 | } 24 | 25 | @Override 26 | protected boolean matchesSafely(T object, Description mismatchDescription) { 27 | mismatchDescription.appendText("properties of " + this.objectName + " did not match\n"); 28 | 29 | return this.matchersByPropertyName.entrySet().stream() 30 | .reduce(true, (state, entry) -> { 31 | String propertyName = entry.getKey(); 32 | Object propertyValue = propertyValue(object, propertyName); 33 | Matcher matcher = entry.getValue(); 34 | 35 | if (!(matcher.matches(propertyValue))) { 36 | mismatchDescription.appendText(INDENTATION + "'" + propertyName + "' : "); 37 | matcher.describeMismatch(propertyValue, mismatchDescription); 38 | mismatchDescription.appendText("\n"); 39 | 40 | return false; 41 | } 42 | 43 | return state; 44 | }, (a, b) -> a && b); 45 | } 46 | 47 | @Override 48 | public void describeTo(Description description) { 49 | description.appendText(this.objectName + " with state\n" + describe(this.matchersByPropertyName)); 50 | } 51 | 52 | public static , B extends MatcherBuilder> M hasState(B matcherBuilder) { 53 | return matcherBuilder.build(); 54 | } 55 | 56 | private static String describe(Map> matchersByPropertyName) { 57 | return matchersByPropertyName.entrySet().stream() 58 | .map((entry) -> "'" + entry.getKey() + "' : " + entry.getValue()) 59 | .collect(joining("\n" + INDENTATION, INDENTATION, "")); 60 | } 61 | 62 | 63 | protected static abstract class MatcherBuilder> { 64 | 65 | private Map> matchersByPropertyName; 66 | 67 | protected MatcherBuilder() { 68 | this.matchersByPropertyName = new LinkedHashMap<>(); 69 | } 70 | 71 | M build() { 72 | return build(this.matchersByPropertyName); 73 | } 74 | 75 | protected abstract M build(Map> matchersByPropertyName); 76 | 77 | protected > B recordPropertyMatcher(B matcherBuilder, String propertyName, Matcher propertyMatcher) { 78 | this.matchersByPropertyName.put(propertyName, propertyMatcher); 79 | 80 | return matcherBuilder; 81 | } 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-support/src/main/java/com/github/cstettler/dddttc/support/infrastructure/event/PendingDomainEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.support.infrastructure.event; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.jms.core.JmsTemplate; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | import org.springframework.transaction.PlatformTransactionManager; 8 | import org.springframework.transaction.support.TransactionTemplate; 9 | 10 | import javax.jms.ConnectionFactory; 11 | import javax.jms.TextMessage; 12 | import java.util.List; 13 | 14 | import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW; 15 | 16 | class PendingDomainEventPublisher { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(PendingDomainEventPublisher.class); 19 | 20 | private final PendingDomainEventStore pendingDomainEventStore; 21 | private final TransactionTemplate transactionTemplate; 22 | private final JmsTemplate jmsTemplate; 23 | 24 | PendingDomainEventPublisher(PendingDomainEventStore pendingDomainEventStore, PlatformTransactionManager transactionManager, ConnectionFactory connectionFactory) { 25 | this.pendingDomainEventStore = pendingDomainEventStore; 26 | this.transactionTemplate = requiresNewTransactionTemplate(transactionManager); 27 | this.jmsTemplate = topicBasedJmsTemplate(connectionFactory); 28 | } 29 | 30 | @Scheduled(initialDelay = 1000, fixedDelay = 1000) 31 | void triggerDomainEventDispatching() { 32 | loadNextPendingDomainEvents(10).forEach((pendingDomainEvent) -> { 33 | sendPendingDomainEvent(pendingDomainEvent); 34 | markPendingDomainEventDispatched(pendingDomainEvent); 35 | }); 36 | } 37 | 38 | private List loadNextPendingDomainEvents(int count) { 39 | return this.transactionTemplate.execute((status) -> this.pendingDomainEventStore.loadNextPendingDomainEvents(count)); 40 | } 41 | 42 | private void markPendingDomainEventDispatched(PendingDomainEvent pendingDomainEvent) { 43 | this.transactionTemplate.execute((status) -> { 44 | this.pendingDomainEventStore.markPendingDomainEventDispatched(pendingDomainEvent); 45 | 46 | return null; 47 | }); 48 | } 49 | 50 | private void sendPendingDomainEvent(PendingDomainEvent pendingDomainEvent) { 51 | this.jmsTemplate.send(pendingDomainEvent.type(), (session) -> { 52 | TextMessage textMessage = session.createTextMessage(); 53 | textMessage.setStringProperty("domain-event-id", pendingDomainEvent.id()); 54 | textMessage.setText(pendingDomainEvent.payload()); 55 | 56 | return textMessage; 57 | }); 58 | 59 | LOGGER.info("sent message for domain event '" + pendingDomainEvent.id() + "' to '" + pendingDomainEvent.type() + "' (payload '" + pendingDomainEvent.payload() + "'"); 60 | } 61 | 62 | private static TransactionTemplate requiresNewTransactionTemplate(PlatformTransactionManager platformTransactionManager) { 63 | TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager); 64 | transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW); 65 | 66 | return transactionTemplate; 67 | } 68 | 69 | private static JmsTemplate topicBasedJmsTemplate(ConnectionFactory connectionFactory) { 70 | JmsTemplate jmsTemplate = new JmsTemplate(); 71 | jmsTemplate.setConnectionFactory(connectionFactory); 72 | jmsTemplate.setPubSubDomain(true); 73 | 74 | return jmsTemplate; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/main/java/com/github/cstettler/dddttc/accounting/domain/Wallet.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.domain; 2 | 3 | import com.github.cstettler.dddttc.stereotype.Aggregate; 4 | import com.github.cstettler.dddttc.stereotype.AggregateFactory; 5 | import com.github.cstettler.dddttc.stereotype.AggregateId; 6 | import com.github.cstettler.dddttc.support.domain.DomainEventPublisher; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import static com.github.cstettler.dddttc.accounting.domain.Amount.zero; 12 | import static com.github.cstettler.dddttc.accounting.domain.BookingAlreadyBilledException.bookingAlreadyBilled; 13 | import static com.github.cstettler.dddttc.accounting.domain.Transaction.transaction; 14 | import static com.github.cstettler.dddttc.accounting.domain.TransactionReference.transactionReference; 15 | import static com.github.cstettler.dddttc.accounting.domain.TransactionWithSameReferenceAlreadyAppliedException.transactionWithSameReferenceAlreadyApplied; 16 | import static com.github.cstettler.dddttc.accounting.domain.WalletInitializedEvent.walletInitialized; 17 | 18 | @Aggregate 19 | public class Wallet { 20 | 21 | private final WalletOwner walletOwner; 22 | private final List transactions; 23 | private Amount balance; 24 | 25 | private Wallet(WalletOwner walletOwner, DomainEventPublisher domainEventPublisher) { 26 | this.walletOwner = walletOwner; 27 | this.transactions = new ArrayList<>(); 28 | this.balance = zero(); 29 | 30 | domainEventPublisher.publish(walletInitialized(this.walletOwner)); 31 | } 32 | 33 | @AggregateId 34 | public WalletOwner walletOwner() { 35 | return this.walletOwner; 36 | } 37 | 38 | public Amount balance() { 39 | return this.balance; 40 | } 41 | 42 | void chargeAmount(Amount amount, TransactionReference transactionReference) throws TransactionWithSameReferenceAlreadyAppliedException { 43 | applyTransaction(transaction(transactionReference, amount)); 44 | } 45 | 46 | public void billBookingFee(Booking booking, BookingFeePolicy bookingFeePolicy) throws BookingAlreadyBilledException { 47 | try { 48 | Amount bookingFee = bookingFeePolicy.feeForBooking(booking); 49 | TransactionReference transactionReference = transactionReference(booking.id().value()); 50 | 51 | applyTransaction(transaction(transactionReference, bookingFee.negate())); 52 | } catch (TransactionWithSameReferenceAlreadyAppliedException e) { 53 | throw bookingAlreadyBilled(this, booking); 54 | } 55 | } 56 | 57 | @AggregateFactory(Wallet.class) 58 | public static Wallet newWallet(WalletOwner walletOwner, DomainEventPublisher domainEventPublisher) { 59 | return new Wallet(walletOwner, domainEventPublisher); 60 | } 61 | 62 | private Amount recalculateBalance() { 63 | return this.transactions.stream() 64 | .map((transaction) -> transaction.amount()) 65 | .reduce(zero(), (sum, amount) -> sum.add(amount)); 66 | } 67 | 68 | private void applyTransaction(Transaction transaction) { 69 | if (hasAlreadyBeenApplied(transaction)) { 70 | throw transactionWithSameReferenceAlreadyApplied(this.walletOwner, transaction.reference()); 71 | } 72 | 73 | this.transactions.add(transaction); 74 | this.balance = recalculateBalance(); 75 | } 76 | 77 | private boolean hasAlreadyBeenApplied(Transaction transaction) { 78 | return this.transactions.stream() 79 | .anyMatch((existingTransaction) -> existingTransaction.reference().equals(transaction.reference())); 80 | } 81 | 82 | } 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDD-To-The-Code Workshop: Sample Code 2 | 3 | Sample project for "DDD to the Code" workshop. 4 | 5 | ## Disclaimer 6 | 7 | :warning: please do not practice Cargo Cult - when using this sample code in your projects, make sure you understand 8 | that code, and know why you are using it. 9 | 10 | This sample project is for educational purposes only. It does not contain production-ready code. Specifically, the 11 | following aspects are not considered real-world like: 12 | 13 | - shared code in 'ddd-to-the-code-workshop-support' 14 | - in-memory database for persistence of aggregates 15 | - prototypical domain event infrastructure implementation (non-durable subscriptions, no dead letter handling, ...) 16 | - low test coverage in accounting and rental bounded contexts 17 | - no handling of dependencies between domain events (e.g. in case booking is ended and should be billed 18 | based on the 'booking completed' domain event before the wallet has been initialized based on the 'user registration 19 | completed' domain event - currently, handling the 'booking completed' domain event would fail, the domain event would 20 | be redelivered and eventually moved to dead letter queue) 21 | 22 | ... and a lot more 23 | 24 | 25 | ## Overview 26 | 27 | ![Bounded Contexts and Use Cases](docs/bounded-context-and-use-cases.png) 28 | 29 | ### Use Cases 30 | - register new user 31 | - initialize wallet for user with welcome amount 32 | - book and return bike 33 | - bill booking fee on wallet 34 | 35 | 36 | ## Demo 37 | 38 | Use the following script to walk through the implemented use cases: 39 | 40 | ### Preparation 41 | 42 | *Note*: due to compatibility issues with AspectJ and Java 11, this project has to be compiled and executed using Java 8. 43 | 44 | - build project: 45 | `./mvnw clean package` 46 | - start registration bounded context: 47 | `java -jar ddd-to-the-code-workshop-registration/target/ddd-to-the-code-workshop-registration-0.0.1-SNAPSHOT.jar` 48 | - start rental bounded context: 49 | `java -jar ddd-to-the-code-workshop-rental/target/ddd-to-the-code-workshop-rental-0.0.1-SNAPSHOT.jar` 50 | - start accounting bounded context: 51 | `java -jar ddd-to-the-code-workshop-accounting/target/ddd-to-the-code-workshop-accounting-0.0.1-SNAPSHOT.jar` 52 | 53 | ### Initial State 54 | - list of existing bikes (): all bikes available 55 | - list of wallets (): no wallets existing 56 | - list of bookings (): no bookings existing 57 | 58 | ### Register New User 59 | - start user registration () 60 | - enter user handle (e.g. "peter") 61 | - click "Next >" button 62 | - read verification code from console of registration bounded context (6 digits code) 63 | - enter verification code 64 | - click "Next >" button 65 | - enter first and last name (e.g. "Peter" and "Meier") 66 | - click "Complete" button 67 | - check for new wallet () with initial amount 68 | 69 | ### Book Bike 70 | - choose bike () 71 | - enter user handle (e.g. "peter") 72 | - click "Book" button 73 | - check list of booking (): new booking (still running) 74 | - check list of bikes (): chosen bike not available for booking 75 | - wait some time 76 | 77 | ### Return Bike 78 | - list bookings () 79 | - click "Return Bike" button 80 | - check list of booking (): booking is ended 81 | - check list of bikes (): previously chosen bike again available 82 | - check list of wallets (): booking fee deducted from wallet 83 | -------------------------------------------------------------------------------- /ddd-to-the-code-workshop-accounting/src/test/java/com/github/cstettler/dddttc/accounting/infrastructure/test/WalletScenarioTests.java: -------------------------------------------------------------------------------- 1 | package com.github.cstettler.dddttc.accounting.infrastructure.test; 2 | 3 | import com.github.cstettler.dddttc.accounting.application.WalletService; 4 | import com.github.cstettler.dddttc.accounting.domain.Wallet; 5 | import com.github.cstettler.dddttc.accounting.domain.WalletAlreadyExistsException; 6 | import com.github.cstettler.dddttc.accounting.domain.WalletOwner; 7 | import com.github.cstettler.dddttc.accounting.domain.WalletRepository; 8 | import com.github.cstettler.dddttc.support.test.ScenarioTest; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.function.Executable; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.transaction.PlatformTransactionManager; 13 | import org.springframework.transaction.support.TransactionTemplate; 14 | 15 | import java.util.Collection; 16 | 17 | import static com.github.cstettler.dddttc.accounting.domain.WalletBuilder.wallet; 18 | import static com.github.cstettler.dddttc.accounting.domain.WalletMatcher.walletWith; 19 | import static com.github.cstettler.dddttc.accounting.domain.WalletOwner.walletOwner; 20 | import static com.github.cstettler.dddttc.support.ReflectionBasedStateMatcher.hasState; 21 | import static org.hamcrest.Matchers.hasItem; 22 | import static org.hamcrest.Matchers.hasSize; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.junit.Assert.assertThat; 25 | import static org.junit.jupiter.api.Assertions.assertThrows; 26 | 27 | @ScenarioTest 28 | class WalletScenarioTests { 29 | 30 | @Autowired 31 | private WalletService walletService; 32 | 33 | @Autowired 34 | private WalletRepository walletRepository; 35 | 36 | @Autowired 37 | private PlatformTransactionManager transactionManager; 38 | 39 | @Test 40 | void initializeWallet_nonExistingWallet_createsNewWallet() { 41 | // arrange 42 | WalletOwner walletOwner = walletOwner("peter"); 43 | 44 | // act 45 | this.walletService.initializeWallet(walletOwner); 46 | 47 | // assert 48 | invokeInTransaction(() -> { 49 | Wallet wallet = this.walletRepository.get(walletOwner); 50 | 51 | assertThat(wallet, hasState(walletWith().walletOwner(is(walletOwner)))); 52 | }); 53 | } 54 | 55 | @Test 56 | void initializeWallet_existingWallet_throwsWalletAlreadyExistsException() { 57 | // arrange 58 | WalletOwner walletOwner = walletOwner("peter"); 59 | 60 | invokeInTransaction(() -> this.walletRepository.add(wallet().walletOwner(walletOwner).build())); 61 | 62 | // act + assert 63 | assertThrows(WalletAlreadyExistsException.class, () -> this.walletService.initializeWallet(walletOwner)); 64 | } 65 | 66 | @Test 67 | void listWallets() { 68 | // arrange 69 | invokeInTransaction(() -> { 70 | this.walletRepository.add(wallet().walletOwner("peter").build()); 71 | this.walletRepository.add(wallet().walletOwner("hans").build()); 72 | }); 73 | 74 | // act 75 | Collection wallets = this.walletService.listWallets(); 76 | 77 | assertThat(wallets, hasSize(2)); 78 | assertThat(wallets, hasItem(hasState(walletWith().walletOwner(is(walletOwner("peter")))))); 79 | assertThat(wallets, hasItem(hasState(walletWith().walletOwner(is(walletOwner("hans")))))); 80 | } 81 | 82 | private void invokeInTransaction(Executable executable) { 83 | new TransactionTemplate(this.transactionManager).execute((status) -> { 84 | try { 85 | executable.execute(); 86 | 87 | return null; 88 | } catch (Throwable t) { 89 | throw new IllegalStateException("unable to execute in transaction", t); 90 | } 91 | }); 92 | } 93 | 94 | } 95 | --------------------------------------------------------------------------------