"An exception is abnormal condition that arises in a code sequence at run time. In other words, an exception is a run-time error."
"Java’s exception-handling statements should not be considered a general mechanism for nonlocal branching. If you do so, it will only confuse your code and make it hard to maintain."Java the Complete Reference, Tenth edition
public Result<Error, Account> execute(Account account) {
return accountRepository.findById(account.getId())
.map(a -> Result.fail(a, Error.ACCOUNT_ALREADY_EXIST))
.orElseGet(() -> Result.ok(accountRepository.save(account));
}
public enum Error {
ACCOUNT_ALREADY_EXIST
}
"Command–query separation (CQS) is a principle of imperative computer programming [...] It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, Asking a question should not change the answer."
"Command query responsibility segregation (CQRS) applies the CQS principle by using separate Query and Command objects to retrieve and modify data, respectively."
public interface Command<INPUT, RESULT> {
RESULT execute(INPUT input);
}
@Component
@RequiredArgsConstructor
public class CreateAccountCommand implements Command> {
private final AccountRepository accountRepository;
public Result<Error, Account> execute(Account account) {
return accountRepository.findById(account.getId())
.map(a -> Result.fail(a, Error.ACCOUNT_ALREADY_EXIST))
.orElseGet(() -> save(account));
}
private Result<Error, Account> save(Account account) {
try {
return Result.ok(accountRepository.save(account));
} catch (DataIntegrityViolationException e) {
return Result.fail(account, Error.ACCOUNT_ALREADY_EXIST);
}
}
public enum Error {
ACCOUNT_ALREADY_EXIST
}
}
public interface Query<INPUT, RESULT> {
RESULT execute(INPUT input);
}
@Component
@RequiredArgsConstructor
class GetAccountQuery implements Query<AccountId, Result<Error, Account>> {
private final AccountRepository accountRepository;
@Transactional(readOnly = true)
public Result<Error, Account> execute(AccountId accountId) {
return accountRepository.findById(accountId)
.map(Result::<Error, Account>ok)
.orElse(Result.fail(Error.ACCOUNT_NOT_FOUND));
}
public enum Error {
ACCOUNT_NOT_FOUND
}
}
// TODO remove @JsonProperty when Jackson support records
public record CreateAccountDto(
@NotNull
@JsonProperty("id")
UUID id,
@NotEmpty
@JsonProperty("name")
String name,
@NotNull
@Min(0)
@Max(100)
@JsonProperty("scoring")
Integer scoring
) {
}
@Builder
@RequiredArgsConstructor
@Data
public class CreateAccountDto {
@NotNull
private final UUID id;
@NotBlank
@Max(50)
private final String name;
@Min(0)
@Max(100)
private final int scoring;
}
public class CreateAccountDtoToAccountConverter
implements Converter<CreateAccountDto, Account> {
@Override
public Account convert(CreateAccountDto source) {
return Account.builder()
.id(AccountId.from(source.id()))
.name(source.name())
.scoring(source.scoring())
.build();
}
}
@EmbeddedId
@AttributeOverride(name = "value",
column = @Column(name = "id"))
private AccountId id;
await().atMost(10, SECONDS).until(() -> {
// condition
});
@RequiredArgsConstructor
public class AccountTestBuilder extends Account.AccountBuilder {
private final int seed;
public Account.AccountBuilder withTestDefaults() {
return Account.builder()
.id(AccountId.from(seed))
.name(format("name.%d", seed))
.scoring(seed)
.version(seed);
}
}