code links added to readme
This commit is contained in:
42
README.md
42
README.md
@@ -5,9 +5,10 @@ Not every piece of software is equally important...
|
||||
Not every piece will decide about company / product success or can cause not reversible
|
||||
negative business consequences like materialise brand risk or money loses.
|
||||
On the other hand scalability or non functional requirements are different for different activities in software.
|
||||
|
||||
To accommodate to those differences, separate architectural patterns are applied:
|
||||
|
||||

|
||||

|
||||
|
||||
**Simple Create Read Update Delete functionality** are exposed with leverage of CRUD framework.
|
||||
|
||||
@@ -16,6 +17,11 @@ Goals of that approach:
|
||||
- fast respond to typical changes (ex. „please add another 2 fields on UI”),
|
||||
- exposure of high quality API.
|
||||
|
||||
Examples in code:
|
||||
- CRUD-able document [ProductDescription](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescription.java)
|
||||
- persistence of document [ProductDescriptionEntity](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionEntity.java)
|
||||
- CRUD exposed as DAO and REST endpoint [ProductDescriptionDao](product-management-adapters/src/main/java/pl/com/bottega/factory/product/management/ProductDescriptionDao.java)
|
||||
|
||||
**Complex Commands (business processing)** expressed in Domain Model which is embedded in hexagonal architecture.
|
||||
|
||||
Goals of that approach:
|
||||
@@ -25,6 +31,25 @@ caused by technological choices or transport models from external services / con
|
||||
- make the core business of application technology agnostic, enabling continues technology
|
||||
migration and keeping long living projects up to date with fast evolving frameworks and libraries.
|
||||
|
||||
Examples of Domain Model in code:
|
||||
- aggregate [ProductDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemand.java)
|
||||
- entity [DailyDemand](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DailyDemand.java)
|
||||
- value object [Adjustment](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/Adjustment.java)
|
||||
- policy [ReviewPolicy](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ReviewPolicy.java)
|
||||
- domain event [DemandedLevelsChanged](shared-kernel-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandedLevelsChanged.java)
|
||||
|
||||
Examples of Ports in code:
|
||||
- application service (primary port) [DemandService](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandService.java)
|
||||
- repository (secondary port) [ProductDemandRepository](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandRepository.java)
|
||||
- domain events handling (secondary port) [DemandEvents](demand-forecasting-model/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEvents.java)
|
||||
|
||||
Examples of Adapters in code:
|
||||
- REST endpoint for complex command (driving adapter)
|
||||
- command resource [DemandAdjustmentDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/DemandAdjustmentDao.java)
|
||||
- command handler [CommandsHandler](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/command/CommandsHandler.java)
|
||||
- repository implementation (driven adapter) [ProductDemandORMRepository](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/demand/forecasting/ProductDemandORMRepository.java)
|
||||
- events propagation (driven adapter) [DemandEventsPropagation](app-monolith/src/main/java/pl/com/bottega/factory/demand/forecasting/DemandEventsPropagation.java)
|
||||
|
||||
**Complex Query** implemented as direct and simple as possible by:
|
||||
- fetching persistent read model expected by consumer, the read model is a projection of past domain event,
|
||||
- read model composed at query execution time build directly from persistent form of Domain Model,
|
||||
@@ -34,15 +59,20 @@ Additional complex calculations or projections can be partially delegated to the
|
||||
|
||||
Goals of that approach:
|
||||
- encapsulation of the Domain Model complexity by providing (simpler) consumer driven or published language API,
|
||||
- freeing Domain the Model from exposing data for reads making the Domain Model simpler,
|
||||
- freeing the Domain Model from exposing data for reads making the Domain Model simpler,
|
||||
- improves reads performance and enable horizontal scalability.
|
||||
|
||||
Examples in code:
|
||||
- projection of domain events to persistent read model [DeliveryForecastProjection](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastProjection.java)
|
||||
- REST endpoint for persistent read model [DeliveryForecastDao](demand-forecasting-adapters/src/main/java/pl/com/bottega/factory/delivery/planning/projection/DeliveryForecastDao.java)
|
||||
- read model composed at query execution time [StockForecastQuery](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/StockForecastQuery.java)
|
||||
- REST resource processor for NOT persistent read model [StockForecastResourceProcessor](app-monolith/src/main/java/pl/com/bottega/factory/stock/forecast/ressource/StockForecastResourceProcessor.java)
|
||||
|
||||
## Hexagonal Architecture
|
||||
Only the most valuable part of that enterprise software is embedded in hexagonal architecture -
|
||||
complex business processing modeled in form of the Domain Model.
|
||||
|
||||

|
||||

|
||||
|
||||
**Application Services** - providing entry point to Domain Model functionality,
|
||||
Application Services are ports for Primary / Driving Adapters like RESTfull endpoints.
|
||||
@@ -69,13 +99,13 @@ with **Model Exploration Whirlpool** and build **Ubiquitous Language** with your
|
||||
Adding infrastructure and technology later is easy thanks to Hexagonal Architecture.
|
||||
|
||||
Simply starting from ZERO business knowledge through initial domain and opportunity exploration with **Big Picture Event Storming**:
|
||||

|
||||

|
||||
|
||||
after cleaning and trimming initial model to most valuable and needed areas:
|
||||

|
||||

|
||||
|
||||
Deep dive in **Demand Forecasting** sub-domain with **Design Level Event Storming**:
|
||||

|
||||

|
||||
|
||||
is excellent canvas to cooperative exploration of:
|
||||
- impacted and required actors,
|
||||
|
||||
@@ -10,13 +10,13 @@ import pl.com.bottega.factory.delivery.planning.projection.DeliveryForecastProje
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@RepositoryEventHandler
|
||||
public class DeliveryPlannerDefinitionEventsMapping {
|
||||
public class DeliveryPlannerDefinitionEventsPropagation {
|
||||
|
||||
private DeliveryForecastProjection projection;
|
||||
|
||||
@HandleAfterSave
|
||||
@HandleAfterCreate
|
||||
public void handle(DeliveryPlannerDefinitionEntity entity) {
|
||||
projection.handleDeliveryPlannerDefinitionChange(entity.getRefNo());
|
||||
public void handleCreateAndUpdate(DeliveryPlannerDefinitionEntity entity) {
|
||||
projection.applyDeliveryPlannerDefinitionChange(entity.getRefNo());
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
||||
@Lazy
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
class DemandEventsMapping implements DemandEvents {
|
||||
class DemandEventsPropagation implements DemandEvents {
|
||||
|
||||
private final CurrentDemandProjection demandProjection;
|
||||
private final DeliveryForecastProjection deliveryProjection;
|
||||
@@ -26,8 +26,8 @@ class DemandEventsMapping implements DemandEvents {
|
||||
|
||||
@Override
|
||||
public void emit(DemandedLevelsChanged event) {
|
||||
demandProjection.persistCurrentDemands(event);
|
||||
deliveryProjection.persistDeliveryForecasts(event);
|
||||
demandProjection.applyDemandedLevelsChanged(event);
|
||||
deliveryProjection.applyDemandedLevelsChanged(event);
|
||||
shortagePrediction.predictShortages(event);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package pl.com.bottega.factory.product.management;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.rest.core.annotation.HandleAfterCreate;
|
||||
import org.springframework.data.rest.core.annotation.HandleAfterDelete;
|
||||
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.demand.forecasting.DemandService;
|
||||
@@ -11,14 +12,19 @@ import pl.com.bottega.factory.stock.forecast.ressource.StockForecastEntity;
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@RepositoryEventHandler
|
||||
public class ProductDescriptionEventsMapping {
|
||||
public class ProductDescriptionEventsPropagation {
|
||||
|
||||
private final DemandService demandService;
|
||||
private final StockForecastDao stockForecasts;
|
||||
|
||||
@HandleAfterCreate
|
||||
public void handle(ProductDescriptionEntity entity) {
|
||||
public void handleCreate(ProductDescriptionEntity entity) {
|
||||
demandService.init(entity.getRefNo());
|
||||
stockForecasts.save(new StockForecastEntity(entity.getRefNo()));
|
||||
}
|
||||
|
||||
@HandleAfterDelete
|
||||
public void handleDelete(ProductDescriptionEntity entity) {
|
||||
stockForecasts.delete(entity.getRefNo());
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import pl.com.bottega.factory.shortages.prediction.notification.NotificationOfSh
|
||||
@Lazy
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
class ShortageEventsMapping implements ShortageEvents {
|
||||
class ShortageEventsPropagation implements ShortageEvents {
|
||||
|
||||
private final NotificationOfShortage notification;
|
||||
|
||||
@@ -31,7 +31,7 @@ public class StockForecastQuery {
|
||||
public StockForecast get(RefNoId refNo) {
|
||||
Stock stock = stocks.forRefNo(refNo);
|
||||
LocalDate today = LocalDate.now(clock);
|
||||
return build(refNo, today, stock,
|
||||
return build(today, stock,
|
||||
this.demands
|
||||
.findByRefNoAndDateGreaterThanEqual(refNo.getRefNo(), today).stream()
|
||||
.collect(toMap(
|
||||
@@ -47,7 +47,7 @@ public class StockForecastQuery {
|
||||
);
|
||||
}
|
||||
|
||||
private StockForecast build(RefNoId refNo, LocalDate today,
|
||||
private StockForecast build(LocalDate today,
|
||||
Stock stock,
|
||||
Map<LocalDate, Long> demands,
|
||||
Map<LocalDate, Long> outputs) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package pl.com.bottega.factory.stock.forecast.ressource;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
import pl.com.bottega.factory.stock.forecast.StockForecast;
|
||||
import pl.com.bottega.factory.stock.forecast.StockForecastQuery;
|
||||
|
||||
@Component
|
||||
class StaticAccess {
|
||||
|
||||
private static StockForecastQuery query;
|
||||
|
||||
StaticAccess(StockForecastQuery query) {
|
||||
StaticAccess.query = query;
|
||||
}
|
||||
|
||||
static StockForecast calculateQuery(String refNo) {
|
||||
return query.get(new RefNoId(refNo));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package pl.com.bottega.factory.stock.forecast.ressource;
|
||||
|
||||
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||
import org.springframework.data.rest.core.config.Projection;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import pl.com.bottega.tools.ProjectionRepository;
|
||||
|
||||
@@ -8,7 +9,12 @@ import pl.com.bottega.tools.ProjectionRepository;
|
||||
@RepositoryRestResource(path = "stock-forecasts",
|
||||
collectionResourceRel = "stock-forecasts",
|
||||
itemResourceRel = "stock-forecast",
|
||||
excerptProjection = StockForecastEntity.CollectionItem.class)
|
||||
excerptProjection = StockForecastDao.CollectionItem.class)
|
||||
public interface StockForecastDao extends ProjectionRepository<StockForecastEntity, String> {
|
||||
|
||||
@Projection(types = {StockForecastEntity.class})
|
||||
interface CollectionItem {
|
||||
String getRefNo();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package pl.com.bottega.factory.stock.forecast.ressource;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.rest.core.config.Projection;
|
||||
import lombok.Setter;
|
||||
import pl.com.bottega.factory.stock.forecast.StockForecast;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
@@ -18,17 +18,10 @@ public class StockForecastEntity implements Serializable {
|
||||
|
||||
@Id
|
||||
private String refNo;
|
||||
@Setter
|
||||
private transient StockForecast stockForecast;
|
||||
|
||||
public StockForecastEntity(String refNo) {
|
||||
this.refNo = refNo;
|
||||
}
|
||||
|
||||
public StockForecast getStockForecast() {
|
||||
return StaticAccess.calculateQuery(refNo);
|
||||
}
|
||||
|
||||
@Projection(types = {StockForecastEntity.class})
|
||||
interface CollectionItem {
|
||||
String getRefNo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package pl.com.bottega.factory.stock.forecast.ressource;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceProcessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pl.com.bottega.factory.product.management.RefNoId;
|
||||
import pl.com.bottega.factory.stock.forecast.StockForecastQuery;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
class StockForecastResourceProcessor implements ResourceProcessor<Resource<StockForecastEntity>> {
|
||||
|
||||
private final StockForecastQuery query;
|
||||
|
||||
@Override
|
||||
public Resource<StockForecastEntity> process(Resource<StockForecastEntity> resource) {
|
||||
resource.getContent().setStockForecast(
|
||||
query.get(new RefNoId(resource.getContent().getRefNo()))
|
||||
);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 177 KiB |
@@ -22,7 +22,7 @@ public class DeliveryForecastProjection {
|
||||
private final CurrentDemandDao demandDao;
|
||||
private final DeliveryAutoPlannerORMRepository planners;
|
||||
|
||||
public void persistDeliveryForecasts(DemandedLevelsChanged event) {
|
||||
public void applyDemandedLevelsChanged(DemandedLevelsChanged event) {
|
||||
DeliveryAutoPlanner planner = planners.get(event.getRefNo().getRefNo());
|
||||
event.getResults().keySet()
|
||||
.forEach(daily -> forecastDao.deleteByRefNoAndDate(
|
||||
@@ -42,7 +42,7 @@ public class DeliveryForecastProjection {
|
||||
);
|
||||
}
|
||||
|
||||
public void handleDeliveryPlannerDefinitionChange(String refNo) {
|
||||
public void applyDeliveryPlannerDefinitionChange(String refNo) {
|
||||
List<CurrentDemandEntity> demands = demandDao.findByRefNoAndDateGreaterThanEqual(refNo, LocalDate.now(clock));
|
||||
DeliveryAutoPlanner planner = planners.get(refNo);
|
||||
forecastDao.deleteByRefNo(refNo);
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.time.LocalDate;
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@RepositoryEventHandler
|
||||
public class Handler {
|
||||
public class CommandsHandler {
|
||||
|
||||
private final DemandService service;
|
||||
private final DemandAdjustmentDao adjustments;
|
||||
@@ -10,7 +10,7 @@ public class CurrentDemandProjection {
|
||||
|
||||
private final CurrentDemandDao demandDao;
|
||||
|
||||
public void persistCurrentDemands(DemandedLevelsChanged event) {
|
||||
public void applyDemandedLevelsChanged(DemandedLevelsChanged event) {
|
||||
event.getResults().forEach((daily, change) -> {
|
||||
demandDao.deleteByRefNoAndDate(
|
||||
daily.getRefNo(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
import lombok.AllArgsConstructor;
|
||||
import pl.com.bottega.factory.demand.forecasting.DailyDemand.Result;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -11,12 +11,12 @@ import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Value
|
||||
@AllArgsConstructor
|
||||
public class AdjustDemand {
|
||||
String refNo;
|
||||
Map<LocalDate, Adjustment> adjustments;
|
||||
private final String refNo;
|
||||
private final Map<LocalDate, Adjustment> adjustments;
|
||||
|
||||
public List<Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Adjustment, Result> f) {
|
||||
List<Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Adjustment, Result> f) {
|
||||
return adjustments.entrySet().stream()
|
||||
.filter(e -> !e.getKey().isBefore(date))
|
||||
.map(e -> f.apply(e.getKey(), e.getValue()))
|
||||
@@ -24,6 +24,11 @@ public class AdjustDemand {
|
||||
}
|
||||
|
||||
public Optional<LocalDate> latestAdjustment() {
|
||||
return adjustments.keySet().stream().max(Comparator.naturalOrder());
|
||||
return adjustments.keySet().stream()
|
||||
.max(Comparator.naturalOrder());
|
||||
}
|
||||
|
||||
public String getRefNo() {
|
||||
return refNo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
package pl.com.bottega.factory.demand.forecasting;
|
||||
|
||||
import lombok.Value;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.SortedMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Value
|
||||
@AllArgsConstructor
|
||||
public class Document {
|
||||
|
||||
Instant created;
|
||||
String refNo;
|
||||
SortedMap<LocalDate, Demand> demands;
|
||||
private final Instant created;
|
||||
private final String refNo;
|
||||
private final SortedMap<LocalDate, Demand> demands;
|
||||
|
||||
public Document(Instant created, String refNo, SortedMap<LocalDate, Demand> demands) {
|
||||
this.created = created;
|
||||
this.refNo = refNo;
|
||||
this.demands = Collections.unmodifiableSortedMap(demands);
|
||||
}
|
||||
|
||||
public List<DailyDemand.Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Demand, DailyDemand.Result> f) {
|
||||
List<DailyDemand.Result> forEachStartingFrom(LocalDate date, BiFunction<LocalDate, Demand, DailyDemand.Result> f) {
|
||||
return demands.entrySet().stream()
|
||||
.filter(e -> !e.getKey().isBefore(date))
|
||||
.map(e -> f.apply(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public String getRefNo() {
|
||||
return refNo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
hexagon.png
BIN
hexagon.png
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 296 KiB |
Reference in New Issue
Block a user