Skip to content

Commit 6374d09

Browse files
committed
Spring,Dropwizard: Add transfer operation impl
1 parent 45a1fca commit 6374d09

File tree

7 files changed

+304
-0
lines changed

7 files changed

+304
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package bitxon.api.constant;
2+
3+
import lombok.experimental.UtilityClass;
4+
5+
@UtilityClass
6+
public class Constants {
7+
8+
public static final String DIRTY_TRICK_HEADER = "Dirty-Trick-Header";
9+
10+
11+
@UtilityClass
12+
public static class DirtyTrick {
13+
public static final String FAIL_TRANSFER = "FAIL_TRANSFER";
14+
}
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package bitxon.api.model;
2+
3+
import javax.validation.constraints.Min;
4+
import javax.validation.constraints.NotNull;
5+
import javax.validation.constraints.PositiveOrZero;
6+
7+
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
9+
import lombok.Data;
10+
import lombok.NoArgsConstructor;
11+
12+
@Data
13+
@Builder
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class MoneyTransfer {
17+
@Min(1)
18+
@NotNull
19+
Long senderId;
20+
@Min(1)
21+
@NotNull
22+
Long recipientId;
23+
@PositiveOrZero
24+
Integer moneyAmount;
25+
}

dropwizard-app/src/main/java/bitxon/dropwizard/resource/AccountResource.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package bitxon.dropwizard.resource;
22

3+
import static bitxon.api.constant.Constants.DIRTY_TRICK_HEADER;
4+
import static bitxon.api.constant.Constants.DirtyTrick.FAIL_TRANSFER;
5+
36
import javax.inject.Inject;
47
import javax.validation.Valid;
58
import javax.validation.constraints.NotNull;
69
import javax.ws.rs.GET;
10+
import javax.ws.rs.HeaderParam;
711
import javax.ws.rs.POST;
812
import javax.ws.rs.Path;
913
import javax.ws.rs.PathParam;
@@ -15,6 +19,7 @@
1519
import java.util.stream.Collectors;
1620

1721
import bitxon.api.model.Account;
22+
import bitxon.api.model.MoneyTransfer;
1823
import bitxon.dropwizard.db.AccountDao;
1924
import bitxon.dropwizard.mapper.AccountMapper;
2025
import io.dropwizard.hibernate.UnitOfWork;
@@ -55,4 +60,27 @@ public Account create(@NotNull @Valid Account account) {
5560
.map(mapper::mapToApi)
5661
.get();
5762
}
63+
64+
@POST
65+
@Path("/transfers")
66+
@UnitOfWork
67+
public void transfer(@NotNull @Valid MoneyTransfer transfer,
68+
@HeaderParam(DIRTY_TRICK_HEADER) String dirtyTrick) {
69+
70+
var sender = dao.findById(transfer.getSenderId())
71+
.orElseThrow(() -> new RuntimeException("Sender not found"));
72+
73+
var recipient = dao.findById(transfer.getRecipientId())
74+
.orElseThrow(() -> new RuntimeException("Recipient not found"));
75+
76+
sender.setMoneyAmount(sender.getMoneyAmount() - transfer.getMoneyAmount());
77+
dao.save(sender);
78+
79+
if (FAIL_TRANSFER.equals(dirtyTrick)) {
80+
throw new RuntimeException("Error during money transfer");
81+
}
82+
83+
recipient.setMoneyAmount(recipient.getMoneyAmount() + transfer.getMoneyAmount());
84+
dao.save(recipient);
85+
}
5886
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package bitxon.dropwizard;
2+
3+
import static bitxon.api.constant.Constants.DIRTY_TRICK_HEADER;
4+
import static bitxon.api.constant.Constants.DirtyTrick.FAIL_TRANSFER;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
import javax.ws.rs.client.Entity;
8+
9+
import bitxon.api.model.Account;
10+
import bitxon.api.model.MoneyTransfer;
11+
import org.junit.jupiter.api.Test;
12+
13+
public class MoneyTransferDropwizardTest extends AbstractDropwizardTest {
14+
15+
@Test
16+
void transfer() throws Exception {
17+
var transferAmount = 40;
18+
// Sender
19+
var senderId = 1L;
20+
var senderMoneyAmountOriginal = 340;
21+
var senderMoneyAmountResult = senderMoneyAmountOriginal - transferAmount;
22+
// Recipient
23+
var recipientId = 2L;
24+
var recipientMoneyAmountOriginal = 573;
25+
var recipientMoneyAmountResult = recipientMoneyAmountOriginal + transferAmount;
26+
27+
var requestBody = MoneyTransfer.builder()
28+
.senderId(senderId)
29+
.recipientId(recipientId)
30+
.moneyAmount(transferAmount)
31+
.build();
32+
33+
var response = client()
34+
.target(String.format("http://localhost:%d/accounts/transfers", appLocalPort()))
35+
.request()
36+
.buildPost(Entity.entity(requestBody, "application/json"))
37+
.submit()
38+
.get();
39+
40+
assertThat(response.getStatus()).isEqualTo(204);
41+
assertThat(retrieveUserMoneyAmount(senderId)).isEqualTo(senderMoneyAmountResult);
42+
assertThat(retrieveUserMoneyAmount(recipientId)).isEqualTo(recipientMoneyAmountResult);
43+
}
44+
45+
@Test
46+
void transferWithServerProblemDuringTransfer() throws Exception {
47+
var transferAmount = 40;
48+
// Sender
49+
var senderId = 3L;
50+
var senderMoneyAmountOriginal = 79;
51+
// Recipient
52+
var recipientId = 4L;
53+
var recipientMoneyAmountOriginal = 33;
54+
55+
var requestBody = MoneyTransfer.builder()
56+
.senderId(senderId)
57+
.recipientId(recipientId)
58+
.moneyAmount(transferAmount)
59+
.build();
60+
61+
var response = client()
62+
.target(String.format("http://localhost:%d/accounts/transfers", appLocalPort()))
63+
.request()
64+
.header(DIRTY_TRICK_HEADER, FAIL_TRANSFER)
65+
.buildPost(Entity.entity(requestBody, "application/json"))
66+
.submit()
67+
.get();
68+
69+
assertThat(response.getStatus()).isEqualTo(500);
70+
assertThat(retrieveUserMoneyAmount(senderId)).isEqualTo(senderMoneyAmountOriginal);
71+
assertThat(retrieveUserMoneyAmount(recipientId)).isEqualTo(recipientMoneyAmountOriginal);
72+
}
73+
74+
private int retrieveUserMoneyAmount(Long id) {
75+
var response = client()
76+
.target(String.format("http://localhost:%d/accounts/%d", appLocalPort(), id))
77+
.request()
78+
.get();
79+
if (response.getStatus() != 200) {
80+
throw new RuntimeException("Unable to retrieve account to verify result money amount");
81+
}
82+
var account = response.readEntity(Account.class);
83+
return account.getMoneyAmount();
84+
}
85+
}

loadtest/src/gatling/java/gatling/simulation/CommonSimulation.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import java.util.Iterator;
77
import java.util.Map;
8+
import java.util.Random;
89
import java.util.UUID;
910
import java.util.function.Supplier;
1011
import java.util.stream.Stream;
@@ -21,6 +22,13 @@ public class CommonSimulation extends Simulation {
2122
)
2223
).iterator();
2324

25+
static Iterator<Map<String, Object>> feederTransfer = Stream.generate((Supplier<Map<String, Object>>) () ->
26+
Map.of(
27+
"moneyAmount", new Random().nextInt(100)
28+
)
29+
).iterator();
30+
31+
2432
//-----------------------------------------------------------------------------------------------------------------
2533

2634
private static ChainBuilder postAccount(String sessionFieldNameForId) {
@@ -40,6 +48,20 @@ private static ChainBuilder getAllAccounts() {
4048
return exec(http("Get All").get("/"));
4149
}
4250

51+
private static ChainBuilder postTransfer() {
52+
return exec().feed(feederTransfer).exec(http("Transfer")
53+
.post("/transfers")
54+
.header("Content-Type", "application/json")
55+
.body(StringBody("""
56+
{
57+
"senderId": "#{senderId}",
58+
"recipientId": "#{recipientId}",
59+
"moneyAmount": #{moneyAmount}
60+
}
61+
"""))
62+
);
63+
}
64+
4365
//-----------------------------------------------------------------------------------------------------------------
4466

4567
HttpProtocolBuilder httpProtocol = http.baseUrl("http://localhost:8080/accounts")
@@ -57,6 +79,12 @@ private static ChainBuilder getAllAccounts() {
5779
getOneAccountById()
5880
);
5981

82+
ScenarioBuilder scenarioTransfer = scenario("Transfer - Scenario").exec(
83+
postAccount("senderId"),
84+
postAccount("recipientId"),
85+
postTransfer()
86+
);
87+
6088
//-----------------------------------------------------------------------------------------------------------------
6189

6290
{
@@ -73,6 +101,13 @@ private static ChainBuilder getAllAccounts() {
73101
rampUsersPerSec(10).to(20).during(10), // 10 -> 20
74102
rampUsersPerSec(10).to(20).during(10).randomized(), // 10 -> 20
75103
stressPeakUsers(400).during(20) // 8
104+
),
105+
scenarioTransfer.injectOpen(
106+
incrementUsersPerSec(3)
107+
.times(6)
108+
.eachLevelLasting(8)
109+
.separatedByRampsLasting(8)
110+
.startingFrom(8)
76111
)
77112
).protocols(httpProtocol);
78113
}

spring-app/src/main/java/bitxon/spring/controller/AccountController.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package bitxon.spring.controller;
22

3+
import static bitxon.api.constant.Constants.DIRTY_TRICK_HEADER;
4+
import static bitxon.api.constant.Constants.DirtyTrick.FAIL_TRANSFER;
5+
36
import javax.validation.Valid;
47
import java.util.List;
58
import java.util.Optional;
69
import java.util.stream.Collectors;
710

811
import bitxon.api.model.Account;
12+
import bitxon.api.model.MoneyTransfer;
913
import bitxon.spring.db.AccountDao;
1014
import bitxon.spring.mapper.AccountMapper;
1115
import lombok.RequiredArgsConstructor;
16+
import org.springframework.http.HttpStatus;
1217
import org.springframework.transaction.annotation.Transactional;
1318
import org.springframework.web.bind.annotation.GetMapping;
1419
import org.springframework.web.bind.annotation.PathVariable;
1520
import org.springframework.web.bind.annotation.PostMapping;
1621
import org.springframework.web.bind.annotation.RequestBody;
22+
import org.springframework.web.bind.annotation.RequestHeader;
1723
import org.springframework.web.bind.annotation.RequestMapping;
24+
import org.springframework.web.bind.annotation.ResponseStatus;
1825
import org.springframework.web.bind.annotation.RestController;
1926

2027
@RestController
@@ -50,4 +57,27 @@ public Account create(@Valid @RequestBody Account account) {
5057
.map(mapper::mapToApi)
5158
.get();
5259
}
60+
61+
@PostMapping("/transfers")
62+
@ResponseStatus(HttpStatus.NO_CONTENT)
63+
@Transactional
64+
public void create(@Valid @RequestBody MoneyTransfer transfer,
65+
@RequestHeader(value = DIRTY_TRICK_HEADER, required = false) String dirtyTrick) {
66+
67+
var sender = dao.findById(transfer.getSenderId())
68+
.orElseThrow(() -> new RuntimeException("Sender not found"));
69+
70+
var recipient = dao.findById(transfer.getRecipientId())
71+
.orElseThrow(() -> new RuntimeException("Recipient not found"));
72+
73+
sender.setMoneyAmount(sender.getMoneyAmount() - transfer.getMoneyAmount());
74+
dao.save(sender);
75+
76+
if (FAIL_TRANSFER.equals(dirtyTrick)) {
77+
throw new RuntimeException("Error during money transfer");
78+
}
79+
80+
recipient.setMoneyAmount(recipient.getMoneyAmount() + transfer.getMoneyAmount());
81+
dao.save(recipient);
82+
}
5383
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package bitxon.spring;
2+
3+
import static bitxon.api.constant.Constants.DIRTY_TRICK_HEADER;
4+
import static bitxon.api.constant.Constants.DirtyTrick.FAIL_TRANSFER;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
import bitxon.api.model.Account;
8+
import bitxon.api.model.MoneyTransfer;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.http.HttpEntity;
11+
import org.springframework.http.HttpHeaders;
12+
import org.springframework.http.HttpMethod;
13+
14+
public class MoneyTransferSpringTest extends AbstractSpringTest {
15+
16+
@Test
17+
void transfer() {
18+
var transferAmount = 40;
19+
// Sender
20+
var senderId = 1L;
21+
var senderMoneyAmountOriginal = 340;
22+
var senderMoneyAmountResult = senderMoneyAmountOriginal - transferAmount;
23+
// Recipient
24+
var recipientId = 2L;
25+
var recipientMoneyAmountOriginal = 573;
26+
var recipientMoneyAmountResult = recipientMoneyAmountOriginal + transferAmount;
27+
28+
var requestBody = MoneyTransfer.builder()
29+
.senderId(senderId)
30+
.recipientId(recipientId)
31+
.moneyAmount(transferAmount)
32+
.build();
33+
34+
var response = client()
35+
.postForEntity("/accounts/transfers", requestBody, Void.class);
36+
37+
assertThat(response.getStatusCodeValue()).isEqualTo(204);
38+
assertThat(retrieveUserMoneyAmount(senderId)).as("Check sender result balance")
39+
.isEqualTo(senderMoneyAmountResult);
40+
assertThat(retrieveUserMoneyAmount(recipientId)).as("Check recipient result balance")
41+
.isEqualTo(recipientMoneyAmountResult);
42+
}
43+
44+
@Test
45+
void transferWithError() {
46+
var transferAmount = 40;
47+
// Sender
48+
var senderId = 3L;
49+
var senderMoneyAmountOriginal = 79;
50+
// Recipient
51+
var recipientId = 4L;
52+
var recipientMoneyAmountOriginal = 33;
53+
54+
55+
var requestBody = MoneyTransfer.builder()
56+
.senderId(senderId)
57+
.recipientId(recipientId)
58+
.moneyAmount(transferAmount)
59+
.build();
60+
61+
62+
var headers = new HttpHeaders();
63+
headers.set(DIRTY_TRICK_HEADER, FAIL_TRANSFER);
64+
var request = new HttpEntity<MoneyTransfer>(requestBody, headers);
65+
66+
var response = client()
67+
.postForEntity("/accounts/transfers", request, Void.class);
68+
69+
assertThat(response.getStatusCodeValue()).isEqualTo(500);
70+
assertThat(retrieveUserMoneyAmount(senderId)).as("Check sender result balance")
71+
.isEqualTo(senderMoneyAmountOriginal);
72+
assertThat(retrieveUserMoneyAmount(recipientId)).as("Check recipient result balance")
73+
.isEqualTo(recipientMoneyAmountOriginal);
74+
75+
}
76+
77+
private int retrieveUserMoneyAmount(Long id) {
78+
var response = client()
79+
.exchange("/accounts/" + id, HttpMethod.GET, null, Account.class);
80+
81+
if (response.getStatusCodeValue() != 200) {
82+
throw new RuntimeException("Unable to retrieve account to verify result money amount");
83+
}
84+
return response.getBody().getMoneyAmount();
85+
}
86+
}

0 commit comments

Comments
 (0)