public class OrdersPage extends WebPage {
@SpringBean
private OrderService orders;
public OrdersPage() {
var list = new WebMarkupContainer("list");
add(list.setOutputMarkupId(true));
var open = LoadableDetachableModel.of(orders::open);
list.add(new ListView<>("orders", open) {
@Override
protected void populateItem(ListItem<Order> item) {
Order order = item.getModelObject();
String id = order.id();
item.add(new Label("customer", order.customer()));
item.add(Oat.Components.button("ship", "Ship", ajax -> {
orders.ship(id);
Oat.toast(ajax, "Shipped", OatVariant.SUCCESS);
ajax.add(list);
}));
}
});
}
}
@Service
@Transactional
public class OrderService {
private final Orders orders;
private final ApplicationEventPublisher events;
OrderService(Orders orders, ApplicationEventPublisher events) {
this.orders = orders;
this.events = events;
}
public List<Order> open() {
return orders.findByShippedAtIsNull();
}
public void ship(String id) {
var order = orders.findById(id).orElseThrow().ship();
orders.save(order);
// billing and shipping react to this; ordering never calls them
events.publishEvent(new OrderShipped(order.id()));
}
}
// Stored as one document, lines included: no join table,
// no mapping layer, no migration script for a new field.
@Document("orders")
public record Order(@Id String id, String customer,
List<Line> lines, Instant shippedAt) {
public record Line(String product, int quantity) {}
static Order open(String customer, Line... lines) {
return new Order(null, customer, List.of(lines), null);
}
Order ship() {
return new Order(id, customer, lines, Instant.now());
}
}
// Orders.java: the whole persistence layer of the slice
interface Orders extends MongoRepository<Order, String> {
List<Order> findByShippedAtIsNull();
}
@Testcontainers
@ApplicationModuleTest
class OrderServiceTests {
// a real MongoDB in Docker; a replica set, so transactions work
@Container
@ServiceConnection
static MongoDBContainer mongo =
new MongoDBContainer("mongo:8").withReplicaSet();
@Autowired OrderService service;
@Autowired Orders orders;
@Test
void shippingAnOrderPublishesOrderShipped(Scenario scenario) {
var order = orders.save(Order.open("Jane Doe",
new Order.Line("Coffee beans", 2)));
scenario.stimulate(() -> service.ship(order.id()))
.andWaitForEventOfType(OrderShipped.class)
.toArriveAndVerify(event ->
assertThat(event.orderId()).isEqualTo(order.id()));
}
}
class ArchitectureTests {
ApplicationModules modules =
ApplicationModules.of(ShopApplication.class);
@Test
void slicesOnlyTalkThroughTheirPublicApi() {
// fails on cycles or access to another slice's internals
modules.verify();
}
@Test
void uiUsesServicesNotRepositories() {
noClasses().that().areAssignableTo(Component.class)
.should().dependOnClassesThat()
.areAssignableTo(Repository.class)
.check(new ClassFileImporter()
.importPackages("com.acme.shop"));
}
@Test
void documentsTheArchitecture() {
new Documenter(modules).writeDocumentation();
}
}