Wow : Modern Reactive CQRS Architecture Microservice development framework based on DDD and EventSourcing
Domain-Driven | Event-Driven | Test-Driven | Declarative-Design | Reactive Programming | Command Query Responsibility Segregation | Event Sourcing
Use Wow Project Template to quickly create a DDD project based on the Wow framework.
- Test Code: Example
- Test Case: Add To Shopping Cart / Create Order
- Command
WaitStrategy
:SENT
、PROCESSED
WaitStrategy
:SENT
Mode, TheAddCartItem
command write request API After 2 minutes of stress testing, the average TPS was 59625, the peak was 82312, and the average response time was 29 ms.
WaitStrategy
:PROCESSED
Mode, TheAddCartItem
command write request API After 2 minutes of stress testing, the average TPS was 18696, the peak was 24141, and the average response time was 239 ms.
WaitStrategy
:SENT
Mode, TheCreateOrder
command write request API After 2 minutes of stress testing, the average TPS was 47838, the peak was 86200, and the average response time was 217 ms.
WaitStrategy
:PROCESSED
Mode, TheCreateOrder
command write request API After 2 minutes of stress testing, the average TPS was 18230, the peak was 25506, and the average response time was 268 ms.
Automatically register the
Command
routing processing function (HandlerFunction
), and developers only need to write the domain model to complete the service development.
Given -> When -> Expect .
- Understanding Domain Driven Design:《Implementing Domain-Driven Design》,《Domain-Driven Design: Tackling Complexity in the Heart of Software》
- Understanding Command Query Responsibility Segregation(CQRS)
- Understanding EventSourcing
- Understanding Reactive Programming
Given -> When -> Expect .
class CartTest {
@Test
fun addCartItem() {
val ownerId = generateGlobalId()
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>(ownerId)
.givenOwnerId(ownerId)
.whenCommand(addCartItem)
.expectNoError()
.expectEventType(CartItemAdded::class.java)
.expectState {
it.items.assert().hasSize(1)
}.expectStateAggregate {
it.ownerId.assert().isEqualTo(ownerId)
}
.verify()
}
@Test
fun givenStateWhenAdd() {
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.givenState(CartState(generateGlobalId()), 1)
.whenCommand(addCartItem)
.expectNoError()
.expectEventType(CartItemAdded::class.java)
.expectState {
it.items.assert().hasSize(1)
}
.verify()
}
@Test
fun addCartItemIfSameProduct() {
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.given(
CartItemAdded(
added = CartItem(
productId = addCartItem.productId,
quantity = 1,
),
),
)
.whenCommand(addCartItem)
.expectNoError()
.expectEventType(CartQuantityChanged::class.java)
.expectState {
it.items.assert().hasSize(1)
it.items.first().quantity.assert().isEqualTo(2)
}
.verify()
}
@Test
fun addCartItemIfUnCreated() {
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.given()
.whenCommand(addCartItem)
.expectNoError()
.expectEventType(CartItemAdded::class.java)
.expectState {
it.items.assert().hasSize(1)
}
.expectStateAggregate {
it.version.assert().isEqualTo(1)
}
.verify()
}
@Test
fun addCartItemGivenMax() {
val events = buildList {
for (i in 0..99) {
add(
CartItemAdded(
added = CartItem(
productId = "productId$i",
quantity = 1,
),
),
)
}
}.toTypedArray()
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.given(*events)
.whenCommand(addCartItem)
.expectErrorType(IllegalArgumentException::class.java)
.expectState {
it.items.assert().hasSize(MAX_CART_ITEM_SIZE)
}
.verify()
}
@Test
fun removeCartItem() {
val removeCartItem = RemoveCartItem(
productIds = setOf("productId"),
)
val added = CartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.given(
CartItemAdded(
added = added,
),
)
.whenCommand(removeCartItem)
.expectEventType(CartItemRemoved::class.java)
.expectState {
it.items.assert().isEmpty()
}
.verify()
}
@Test
fun changeQuantity() {
val changeQuantity = ChangeQuantity(
productId = "productId",
quantity = 2,
)
val added = CartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.given(
CartItemAdded(
added = added,
),
)
.whenCommand(changeQuantity)
.expectEventType(CartQuantityChanged::class.java)
.expectState {
it.items.assert().hasSize(1)
it.items.first().quantity.assert().isEqualTo(changeQuantity.quantity)
}
.verify()
}
@Test
fun onCreateThenDeleteThenRecover() {
val addCartItem = AddCartItem(
productId = "productId",
quantity = 1,
)
aggregateVerifier<Cart, CartState>()
.whenCommand(addCartItem)
.expectNoError()
.expectEventType(CartItemAdded::class.java)
.expectState {
it.items.assert().hasSize(1)
}
.verify()
.then()
.whenCommand(DefaultDeleteAggregate)
.expectEventType(DefaultAggregateDeleted::class.java)
.expectStateAggregate {
it.deleted.assert().isTrue()
}.verify()
.then()
.whenCommand(DefaultDeleteAggregate::class.java)
.expectErrorType(IllegalAccessDeletedAggregateException::class.java)
.verify()
.then()
.whenCommand(DefaultRecoverAggregate)
.expectStateAggregate {
it.deleted.assert().isFalse()
}.verify()
.then()
.whenCommand(DefaultRecoverAggregate)
.expectErrorType(IllegalStateException::class.java)
.verify()
}
}
class CartSagaTest {
@Test
fun onOrderCreated() {
val ownerId = generateGlobalId()
val orderItem = OrderItem(
id = generateGlobalId(),
productId = generateGlobalId(),
price = BigDecimal.valueOf(10),
quantity = 10,
)
sagaVerifier<CartSaga>()
.whenEvent(
event = mockk<OrderCreated> {
every {
items
} returns listOf(orderItem)
every {
fromCart
} returns true
},
ownerId = ownerId
)
.expectCommand<RemoveCartItem> {
it.aggregateId.id.assert().isEqualTo(ownerId)
it.body.productIds.assert().hasSize(1)
it.body.productIds.assert().first().isEqualTo(orderItem.productId)
}
.verify()
}
}
Single Class | Inheritance Pattern | Aggregation Pattern |
---|---|---|