Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

interview! #64

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

</dependencies>

<build>
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/com/devexperts/account/Account.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package com.devexperts.account;

import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

@Builder(builderMethodName = "of")
public class Account {

private final AccountKey accountKey;
private final String firstName;
private final String lastName;
Expand Down Expand Up @@ -32,4 +40,38 @@ public Double getBalance() {
public void setBalance(Double balance) {
this.balance = balance;
}

public void increaseBalance(double amount) {
this.balance += amount;
}

public void decreaseBalance(double amount){
this.balance -= amount;
}

public long getAccountId(){
return accountKey.getAccountId();
}

@Override
public String toString() {
return accountKey.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return new EqualsBuilder()
.append(getAccountKey(), account.getAccountKey())
.isEquals();
}

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(getAccountKey())
.toHashCode();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.devexperts.account;

import lombok.EqualsAndHashCode;

/**
* Unique Account identifier
*
* <p>
* NOTE: we suspect that later {@link #accountId} is not going to be uniquely identifying an account,
* as we might add human-readable account representation and some clearing codes for partners.
* */

@EqualsAndHashCode
public class AccountKey {
private final long accountId;

Expand All @@ -17,4 +21,13 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

public long getAccountId() {
return accountId;
}

@Override
public String toString() {
return "AccountKey{accountId=" + accountId + '}';
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.devexperts.rest;

import com.devexperts.rest.pojo.TransferOperationDTO;
import org.springframework.http.ResponseEntity;

public abstract class AbstractAccountController {
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount);
abstract ResponseEntity<?> transfer(TransferOperationDTO transferOperation);
}
35 changes: 32 additions & 3 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.rest.pojo.TransferOperationDTO;
import com.devexperts.service.AccountService;
import lombok.AllArgsConstructor;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@CommonsLog
@RestController
@RequestMapping("/api")
@AllArgsConstructor
@RequestMapping("/api/operations")
public class AccountController extends AbstractAccountController {

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
private final AccountService accountService;

@PostMapping(value = "/transfer")
public ResponseEntity<?> transfer(@RequestBody @Valid @NotNull TransferOperationDTO transferOperation) {

Account source = accountService.getAccount(transferOperation.getSourceId()),
target = accountService.getAccount(transferOperation.getTargetId());

try {
accountService.transfer(source, target, transferOperation.getAmount());
} catch (RuntimeException exception) {
log.error("Couldn't complete transfer", exception);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage(), exception);
}

log.info("Transfer complete.");
return ResponseEntity.ok("nice!");
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/devexperts/rest/pojo/TransferOperationDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.devexperts.rest.pojo;

import com.devexperts.rest.validation.ExistingAccountId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TransferOperationDTO {

@ExistingAccountId
private Long sourceId;

@ExistingAccountId
private Long targetId;

@NotNull
@Positive
private Double amount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.devexperts.rest.validation;

import com.devexperts.service.AccountService;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

@AllArgsConstructor
public class AccountIdValidator implements ConstraintValidator<ExistingAccountId, Long> {

private final AccountService accountService;

@Override
public boolean isValid(Long id, ConstraintValidatorContext context) {
if(id == null)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Account id cannot be null");
else if(id <= 0)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Account id must be above 0");
else if(accountService.getAccount(id) == null)
throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("Account [%s] not found", id));
else return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.devexperts.rest.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
@Constraint(validatedBy = AccountIdValidator.class)
public @interface ExistingAccountId {

String message() default "Account not exists.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
49 changes: 41 additions & 8 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
import com.devexperts.account.AccountKey;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.devexperts.service.TransferHelper.validateBalance;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();
private final Map<AccountKey, Account> accounts = new ConcurrentHashMap<>();

@Override
public void clear() {
Expand All @@ -19,19 +25,46 @@ public void clear() {

@Override
public void createAccount(Account account) {
accounts.add(account);
accounts.put(account.getAccountKey(), account);
}

@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.findAny()
.orElse(null);
return accounts.get(AccountKey.valueOf(id));
}


@PostConstruct
public void fillAccounts() {
Account
one = Account.of()
.accountKey(AccountKey.valueOf(1))
.balance(5d)
.firstName("Daisy")
.lastName("Duck")
.build(),
two = Account.of()
.accountKey(AccountKey.valueOf(2))
.balance(500000d)
.firstName("Donald")
.lastName("Duck")
.build();

accounts.put(one.getAccountKey(), one);
accounts.put(two.getAccountKey(), two);
}

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
List<Account> locks = Arrays.asList(source, target); //consider ids sequential
locks.sort(Comparator.comparingLong(Account::getAccountId));

synchronized (locks.get(0)) {
synchronized (locks.get(1)) {
validateBalance(source, target, amount);
source.decreaseBalance(amount);
target.increaseBalance(amount);
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/devexperts/service/TransferHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.devexperts.service;

import com.devexperts.account.Account;

public class TransferHelper {
public static void validateBalance(Account source, Account target, double amount) throws RuntimeException{
if(source.getBalance() < amount)
throw new RuntimeException(String.format("[%s] has not enough money", source.getFirstName()));
if(source.equals(target))
throw new IllegalArgumentException("Cannot transfer to the same account");
}
}
8 changes: 8 additions & 0 deletions src/main/resources/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE accounts
(
id BIGINT NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
balance BIGINT NOT NULL,
CONSTRAINT accounts_pkey2 PRIMARY KEY (id)
);
5 changes: 5 additions & 0 deletions src/main/resources/select.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
select source.first_name, concat(coalesce(sum(transfer.amount), 0), ' $') as total_sent
from accounts source inner join transfers transfer on source.id = transfer.source_id
where transfer.transfer_time > '2011-01-01'
group by source.id
having coalesce(sum(transfer.amount), 0) > 1000;
12 changes: 12 additions & 0 deletions src/main/resources/transfers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE transfers
(
id BIGINT NOT NULL,
source_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
amount BIGINT NOT NULL,
transfer_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
CONSTRAINT transfers_pkey2 PRIMARY KEY (id)
);

ALTER TABLE transfers ADD CONSTRAINT transfers_source_id FOREIGN KEY (source_id) REFERENCES accounts (id) ON UPDATE CASCADE ON DELETE RESTRICT;
ALTER TABLE transfers ADD CONSTRAINT transfers_target_id FOREIGN KEY (target_id) REFERENCES accounts (id) ON UPDATE CASCADE ON DELETE RESTRICT;