sequence

This paper mainly analyzes Buckpal’s practice of Hexagonal Architecture

The project structure

├ ─ ─ adapter │ ├ ─ ─ in │ │ └ ─ ─ web │ │ └ ─ ─ SendMoneyController. Java │ └ ─ ─ out │ └ ─ ─ persistence │ ├ ─ ─ AccountJpaEntity. Java │ ├ ─ ─ AccountMapper. Java │ ├ ─ ─ AccountPersistenceAdapter. Java │ ├ ─ ─ ActivityJpaEntity. Java │ ├ ─ ─ ActivityRepository. Java │ └ ─ ─ SpringDataAccountRepository. Java ├ ─ ─ application │ ├ ─ ─ the port │ │ ├ ─ ─ in │ │ │ ├ ─ ─ GetAccountBalanceQuery. Java │ │ │ ├ ─ ─ SendMoneyCommand. Java │ │ │ └ ─ ─ SendMoneyUseCase. Java │ │ └ ─ ─ out │ │ ├ ─ ─ AccountLock. Java │ │ ├ ─ ─ LoadAccountPort. Java │ │ └ ─ ─ UpdateAccountStatePort. Java │ └ ─ ─ service │ ├ ─ ─ GetAccountBalanceService. Java │ ├ ─ ─ MoneyTransferProperties. Java │ ├ ─ ─ NoOpAccountLock. Java │ ├ ─ ─ SendMoneyService. Java │ └ ─ ─ ThresholdExceededException. Java └ ─ ─ domain ├ ─ ─ the Java ├ ─ ─ Activity. The Java ├ ─ ─ ActivityWindow. Java └ ─ ─ Money.javaCopy the code

There are three layers: Adapter, Application and Domain. The application layer defines the port package, which defines two types of interfaces: IN and out. The Adapter layer is also divided into in and out, which respectively implement the interface of application/port layer. The Application service implements the port interface

application/port

in

public interface GetAccountBalanceQuery {

	Money getAccountBalance(AccountId accountId);

}

@Value
@EqualsAndHashCode(callSuper = false)
public
class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {

    @NotNull
    private final AccountId sourceAccountId;

    @NotNull
    private final AccountId targetAccountId;

    @NotNull
    private final Money money;

    public SendMoneyCommand(
            AccountId sourceAccountId,
            AccountId targetAccountId,
            Money money) {
        this.sourceAccountId = sourceAccountId;
        this.targetAccountId = targetAccountId;
        this.money = money;
        this.validateSelf();
    }
}

public interface SendMoneyUseCase {

	boolean sendMoney(SendMoneyCommand command);

}
Copy the code

Application/Port /in defines the GetAccountBalanceQuery and SendMoneyUseCase interfaces

out

public interface AccountLock {

	void lockAccount(Account.AccountId accountId);

	void releaseAccount(Account.AccountId accountId);

}

public interface LoadAccountPort {

	Account loadAccount(AccountId accountId, LocalDateTime baselineDate);
}

public interface UpdateAccountStatePort {

	void updateActivities(Account account);

}
Copy the code

Application/Port/Out defines the AccountLock, LoadAccountPort, and UpdateAccountStatePort interfaces

application/service

@RequiredArgsConstructor
class GetAccountBalanceService implements GetAccountBalanceQuery {

	private final LoadAccountPort loadAccountPort;

	@Override
	public Money getAccountBalance(AccountId accountId) {
		return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
				.calculateBalance();
	}
}

@Component
class NoOpAccountLock implements AccountLock {

	@Override
	public void lockAccount(AccountId accountId) {
		// do nothing
	}

	@Override
	public void releaseAccount(AccountId accountId) {
		// do nothing
	}

}

@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {

	private final LoadAccountPort loadAccountPort;
	private final AccountLock accountLock;
	private final UpdateAccountStatePort updateAccountStatePort;
	private final MoneyTransferProperties moneyTransferProperties;

	@Override
	public boolean sendMoney(SendMoneyCommand command) {

		checkThreshold(command);

		LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);

		Account sourceAccount = loadAccountPort.loadAccount(
				command.getSourceAccountId(),
				baselineDate);

		Account targetAccount = loadAccountPort.loadAccount(
				command.getTargetAccountId(),
				baselineDate);

		AccountId sourceAccountId = sourceAccount.getId()
				.orElseThrow(() -> new IllegalStateException("expected source account ID not to be empty"));
		AccountId targetAccountId = targetAccount.getId()
				.orElseThrow(() -> new IllegalStateException("expected target account ID not to be empty"));

		accountLock.lockAccount(sourceAccountId);
		if (!sourceAccount.withdraw(command.getMoney(), targetAccountId)) {
			accountLock.releaseAccount(sourceAccountId);
			return false;
		}

		accountLock.lockAccount(targetAccountId);
		if (!targetAccount.deposit(command.getMoney(), sourceAccountId)) {
			accountLock.releaseAccount(sourceAccountId);
			accountLock.releaseAccount(targetAccountId);
			return false;
		}

		updateAccountStatePort.updateActivities(sourceAccount);
		updateAccountStatePort.updateActivities(targetAccount);

		accountLock.releaseAccount(sourceAccountId);
		accountLock.releaseAccount(targetAccountId);
		return true;
	}

	private void checkThreshold(SendMoneyCommand command) {
		if(command.getMoney().isGreaterThan(moneyTransferProperties.getMaximumTransferThreshold())){
			throw new ThresholdExceededException(moneyTransferProperties.getMaximumTransferThreshold(), command.getMoney());
		}
	}

}
Copy the code

The application/service GetAccountBalanceService realized application. The port. In the GetAccountBalanceQuery interface; NoOpAccountLock implements the application. The port. Out. AccountLock interface; SendMoneyService implements the application. The port. In. SendMoneyUseCase interface

domain

@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Account { /** * The unique ID of the account. */ @Getter private final AccountId id; /** * The baseline balance of the account. This was the balance of the account before the first * activity in the activityWindow. */ @Getter private final Money baselineBalance; /** * The window of latest activities on this account. */ @Getter private final ActivityWindow activityWindow; /** * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet * persisted. */ public  static Account withoutId( Money baselineBalance, ActivityWindow activityWindow) { return new Account(null, baselineBalance, activityWindow); } /** * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity. */ public static Account withId( AccountId accountId, Money baselineBalance, ActivityWindow activityWindow) { return new Account(accountId, baselineBalance, activityWindow); } public Optional<AccountId> getId(){ return Optional.ofNullable(this.id); } /** * Calculates the total balance of the account by adding the activity values to the baseline balance. */ public Money calculateBalance() { return Money.add( this.baselineBalance, this.activityWindow.calculateBalance(this.id)); } /** * Tries to withdraw a certain amount of money from this account. * If successful, creates a new activity with a negative value. * @return true if the withdrawal was successful, false if not. */ public boolean withdraw(Money money, AccountId targetAccountId) { if (! mayWithdraw(money)) { return false; } Activity withdrawal = new Activity( this.id, this.id, targetAccountId, LocalDateTime.now(), money); this.activityWindow.addActivity(withdrawal); return true; } private boolean mayWithdraw(Money money) { return Money.add( this.calculateBalance(), money.negate()) .isPositiveOrZero(); } /** * Tries to deposit a certain amount of money to this account. * If sucessful, creates a new activity with a positive value. * @return true if the deposit was successful, false if not. */ public boolean deposit(Money money, AccountId sourceAccountId) { Activity deposit = new Activity( this.id, sourceAccountId, this.id, LocalDateTime.now(), money); this.activityWindow.addActivity(deposit); return true; } @Value public static class AccountId { private Long value; } } public class ActivityWindow { /** * The list of account activities within this window. */ private List<Activity> activities; /** * The timestamp of the first activity within this window. */ public LocalDateTime getStartTimestamp() { return activities.stream() .min(Comparator.comparing(Activity::getTimestamp)) .orElseThrow(IllegalStateException::new) .getTimestamp(); } /** * The timestamp of the last activity within this window. * @return */ public LocalDateTime getEndTimestamp() { return activities.stream() .max(Comparator.comparing(Activity::getTimestamp)) .orElseThrow(IllegalStateException::new) .getTimestamp(); } /** * Calculates the balance by summing up the values of all activities within this window. */ public Money calculateBalance(AccountId accountId) { Money depositBalance = activities.stream() .filter(a -> a.getTargetAccountId().equals(accountId)) .map(Activity::getMoney) .reduce(Money.ZERO, Money::add); Money withdrawalBalance = activities.stream() .filter(a -> a.getSourceAccountId().equals(accountId)) .map(Activity::getMoney) .reduce(Money.ZERO, Money::add); return Money.add(depositBalance, withdrawalBalance.negate()); } public ActivityWindow(@NonNull List<Activity> activities) { this.activities = activities; } public ActivityWindow(@NonNull Activity... activities) { this.activities = new ArrayList<>(Arrays.asList(activities)); } public List<Activity> getActivities() { return Collections.unmodifiableList(this.activities); } public void addActivity(Activity activity) { this.activities.add(activity); }}Copy the code

The Account class defines calculateBalance, withdraw, and deposit methods; The ActivityWindow class defines the calculateBalance method

summary

Buckpal engineering Adapter, Application, domain three layers; The application layer defines the port package, which defines two types of interfaces: IN and out. The Adapter layer is also divided into in and out, which respectively implement the interface of application/port layer. The Application service implements the port interface. Domain layer does not depend on any layer. The Port of the application layer defines the interface, and the Service layer implements the interface and reference interface. The Adapter layer implements the interface of the Application’s Port layer.

doc

  • buckpal