Skip to content

Commit c73330c

Browse files
committed
Spring,Dropwizard: Currency Exchange impl
1 parent 6374d09 commit c73330c

31 files changed

+321
-7
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
## Quick start
66
1. Build: `./gradlew clean build`
7-
2. Spin Up DB: `docker-compose -f docker-compose.yml up -d`
7+
2. Spin Up DB And Wiremock: `docker-compose -f docker-compose.yml up -d`
88
3. Start one App: `./run-dropwizard.sh` or `./run-spring.sh`
99
4. Run Gatling: `./gradlew :loadtest:gatlingRun`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package bitxon.api.thirdparty.exchange.model;
2+
3+
import java.util.Map;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class ExchangeRate {
15+
16+
private String base;
17+
private Map<String, Double> rates;
18+
}

common-wiremock/build.gradle

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
group = 'bitxon'
6+
version = '1.0-SNAPSHOT'
7+
sourceCompatibility = '17'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"base": "EUR",
3+
"rates": {
4+
"USD": 1.0,
5+
"GBP": 0.5
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"base": "GBP",
3+
"rates": {
4+
"USD": 2.0,
5+
"EUR": 2.0
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"base": "USD",
3+
"rates": {
4+
"EUR": 1.0,
5+
"GBP": 0.5
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"request": {
3+
"url": "/exchanges?currency=EUR",
4+
"method": "GET"
5+
},
6+
"response": {
7+
"status": 200,
8+
"bodyFileName": "base-eur-response.json",
9+
"headers": {
10+
"Content-Type": "application/json; charset=UTF-8"
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"request": {
3+
"url": "/exchanges?currency=GBP",
4+
"method": "GET"
5+
},
6+
"response": {
7+
"status": 200,
8+
"bodyFileName": "base-gbp-response.json",
9+
"headers": {
10+
"Content-Type": "application/json; charset=UTF-8"
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"request": {
3+
"url": "/exchanges?currency=USD",
4+
"method": "GET"
5+
},
6+
"response": {
7+
"status": 200,
8+
"bodyFileName": "base-usd-response.json",
9+
"headers": {
10+
"Content-Type": "application/json; charset=UTF-8"
11+
}
12+
}
13+
}

docker-compose.yml

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,13 @@ services:
88
- POSTGRES_USER=postgres
99
- POSTGRES_PASSWORD=postgres
1010
ports:
11-
- '5432:5432'
11+
- '5432:5432'
12+
13+
wiremock:
14+
image: wiremock/wiremock:2.32.0
15+
ports:
16+
- "8888:8080"
17+
command:
18+
- "--global-response-templating"
19+
volumes:
20+
- ./common-wiremock/src/main/resources/stubs:/home/wiremock

dropwizard-app/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ dependencies {
2626
runtimeOnly 'org.postgresql:postgresql:42.4.0'
2727

2828
testImplementation 'io.dropwizard:dropwizard-testing:2.1.1'
29+
testImplementation project(":common-wiremock")
30+
testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
2931
testImplementation 'org.testcontainers:testcontainers:1.17.3'
3032
testImplementation 'org.testcontainers:junit-jupiter:1.17.3'
3133
testImplementation 'org.testcontainers:postgresql:1.17.3'

dropwizard-app/src/main/java/bitxon/dropwizard/DropwizardApplication.java

+10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package bitxon.dropwizard;
22

3+
import bitxon.dropwizard.client.exchange.ExchangeClient;
34
import bitxon.dropwizard.customization.ClasspathOrFileConfigurationSourceProvider;
45
import bitxon.dropwizard.db.AccountDao;
56
import bitxon.dropwizard.db.AccountDaoHibernateImpl;
67
import bitxon.dropwizard.db.model.Account;
78
import bitxon.dropwizard.mapper.AccountMapper;
89
import bitxon.dropwizard.resource.AccountResource;
910
import io.dropwizard.Application;
11+
import io.dropwizard.client.JerseyClientBuilder;
1012
import io.dropwizard.db.DataSourceFactory;
1113
import io.dropwizard.db.PooledDataSourceFactory;
1214
import io.dropwizard.hibernate.dual.HibernateBundle;
@@ -50,6 +52,14 @@ protected void configure() {
5052
}
5153
});
5254

55+
56+
var client = new JerseyClientBuilder(environment)
57+
.using(configuration.getExchangeClientConfig())
58+
.build("exchangeClient");
59+
var exchangeClient = new ExchangeClient(client, configuration.getExchangeClientConfig());
60+
environment.jersey().register(exchangeClient);
61+
62+
5363
environment.jersey().register(AccountResource.class);
5464
}
5565

dropwizard-app/src/main/java/bitxon/dropwizard/DropwizardConfiguration.java

+18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package bitxon.dropwizard;
22

33
import javax.validation.Valid;
4+
import javax.validation.constraints.NotBlank;
45
import javax.validation.constraints.NotNull;
56

7+
import com.fasterxml.jackson.annotation.JsonProperty;
68
import io.dropwizard.Configuration;
9+
import io.dropwizard.client.JerseyClientConfiguration;
710
import io.dropwizard.db.DataSourceFactory;
811
import lombok.Data;
912
import lombok.EqualsAndHashCode;
@@ -15,4 +18,19 @@ public class DropwizardConfiguration extends Configuration {
1518
@Valid
1619
@NotNull
1720
private DataSourceFactory database = new DataSourceFactory();
21+
22+
@Valid
23+
@NotNull
24+
private DropwizardConfiguration.ExchangeClientConfig exchangeClientConfig = new ExchangeClientConfig();
25+
26+
27+
@Data
28+
@EqualsAndHashCode(callSuper = false)
29+
public static class ExchangeClientConfig extends JerseyClientConfiguration {
30+
31+
@NotBlank
32+
@JsonProperty("basePath")
33+
String basePath;
34+
35+
}
1836
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package bitxon.dropwizard.client.exchange;
2+
3+
import javax.ws.rs.client.Client;
4+
5+
import bitxon.api.thirdparty.exchange.model.ExchangeRate;
6+
import bitxon.dropwizard.DropwizardConfiguration;
7+
import lombok.RequiredArgsConstructor;
8+
9+
@RequiredArgsConstructor
10+
public class ExchangeClient {
11+
12+
private final Client client;
13+
private final DropwizardConfiguration.ExchangeClientConfig config;
14+
15+
public ExchangeRate getExchangeRate(String currency) {
16+
var response = client.target(config.getBasePath() + currency)
17+
.request()
18+
.get(ExchangeRate.class);
19+
return response;
20+
}
21+
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import bitxon.api.model.Account;
2222
import bitxon.api.model.MoneyTransfer;
23+
import bitxon.dropwizard.client.exchange.ExchangeClient;
2324
import bitxon.dropwizard.db.AccountDao;
2425
import bitxon.dropwizard.mapper.AccountMapper;
2526
import io.dropwizard.hibernate.UnitOfWork;
@@ -33,6 +34,7 @@ public class AccountResource {
3334

3435
private final AccountDao dao;
3536
private final AccountMapper mapper;
37+
private final ExchangeClient exchangeClient;
3638

3739
@GET
3840
@UnitOfWork
@@ -73,14 +75,17 @@ public void transfer(@NotNull @Valid MoneyTransfer transfer,
7375
var recipient = dao.findById(transfer.getRecipientId())
7476
.orElseThrow(() -> new RuntimeException("Recipient not found"));
7577

78+
var exchangeRateValue = exchangeClient.getExchangeRate(sender.getCurrency())
79+
.getRates().getOrDefault(recipient.getCurrency(), 1.0);
80+
7681
sender.setMoneyAmount(sender.getMoneyAmount() - transfer.getMoneyAmount());
7782
dao.save(sender);
7883

7984
if (FAIL_TRANSFER.equals(dirtyTrick)) {
8085
throw new RuntimeException("Error during money transfer");
8186
}
8287

83-
recipient.setMoneyAmount(recipient.getMoneyAmount() + transfer.getMoneyAmount());
88+
recipient.setMoneyAmount(recipient.getMoneyAmount() + (int)(transfer.getMoneyAmount() * exchangeRateValue));
8489
dao.save(recipient);
8590
}
8691
}

dropwizard-app/src/main/resources/config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ database:
1717
#hibernate.show_sql: true
1818
#hibernate.hbm2ddl.import_files:
1919

20+
exchangeClientConfig:
21+
basePath: 'http://localhost:8888/exchanges?currency='
22+
2023
logging:
2124
level: INFO
2225
loggers:

dropwizard-app/src/test/java/bitxon/dropwizard/AbstractDropwizardTest.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import javax.ws.rs.client.Client;
77

88

9+
import com.github.tomakehurst.wiremock.WireMockServer;
10+
import com.github.tomakehurst.wiremock.client.WireMock;
11+
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
912
import io.dropwizard.client.JerseyClientBuilder;
1013
import io.dropwizard.testing.DropwizardTestSupport;
1114
import io.dropwizard.testing.ResourceHelpers;
@@ -16,6 +19,7 @@
1619
public abstract class AbstractDropwizardTest {
1720

1821
private static final PostgreSQLContainer DB;
22+
public static final WireMockServer WIREMOCK;
1923
private static final DropwizardTestSupport<DropwizardConfiguration> APP;
2024
private static final Client CLIENT;
2125

@@ -27,12 +31,20 @@ public abstract class AbstractDropwizardTest {
2731
.withInitScript("sql/db-test-data.sql");
2832
DB.start();
2933

34+
WIREMOCK = new WireMockServer(WireMockConfiguration.options()
35+
.usingFilesUnderClasspath("stubs") // Loading stubs from common-wiremock
36+
.dynamicPort()
37+
);
38+
WIREMOCK.start();
39+
WireMock.configureFor(WIREMOCK.port());
40+
3041
APP = new DropwizardTestSupport<>(DropwizardApplication.class,
3142
ResourceHelpers.resourceFilePath("config-test.yml"),
3243
//config("server.applicationConnectors[0].port", "0"),
3344
config("database.url", DB.getJdbcUrl()),
3445
config("database.user", DB.getUsername()),
35-
config("database.password", DB.getPassword())
46+
config("database.password", DB.getPassword()),
47+
config("exchangeClientConfig.basePath", WIREMOCK.baseUrl() + "/exchanges?currency=")
3648
);
3749
try {
3850
// This trick helps to spin up application once

dropwizard-app/src/test/java/bitxon/dropwizard/MoneyTransferDropwizardTest.java

+31
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ void transfer() throws Exception {
4242
assertThat(retrieveUserMoneyAmount(recipientId)).isEqualTo(recipientMoneyAmountResult);
4343
}
4444

45+
@Test
46+
void transferFromGBPToUSD() throws Exception {
47+
var transferAmount = 40;
48+
var exchangeRate = 2.0d;
49+
// Sender
50+
var senderId = 6L;
51+
var senderMoneyAmountOriginal = 80; // GPB
52+
var senderMoneyAmountResult = senderMoneyAmountOriginal - transferAmount;
53+
// Recipient
54+
var recipientId = 5L;
55+
var recipientMoneyAmountOriginal = 100; // USD
56+
var recipientMoneyAmountResult = recipientMoneyAmountOriginal + (int)(transferAmount * exchangeRate);
57+
58+
var requestBody = MoneyTransfer.builder()
59+
.senderId(senderId)
60+
.recipientId(recipientId)
61+
.moneyAmount(transferAmount)
62+
.build();
63+
64+
var response = client()
65+
.target(String.format("http://localhost:%d/accounts/transfers", appLocalPort()))
66+
.request()
67+
.buildPost(Entity.entity(requestBody, "application/json"))
68+
.submit()
69+
.get();
70+
71+
assertThat(response.getStatus()).isEqualTo(204);
72+
assertThat(retrieveUserMoneyAmount(senderId)).isEqualTo(senderMoneyAmountResult);
73+
assertThat(retrieveUserMoneyAmount(recipientId)).isEqualTo(recipientMoneyAmountResult);
74+
}
75+
4576
@Test
4677
void transferWithServerProblemDuringTransfer() throws Exception {
4778
var transferAmount = 40;

dropwizard-app/src/test/resources/config-test.yml

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ database:
1717
#hibernate.show_sql: true
1818
#hibernate.hbm2ddl.import_files:
1919

20+
exchangeClientConfig:
21+
basePath: 'will-be-replaced-in-test'
22+
2023
logging:
2124
level: INFO
2225
loggers:

dropwizard-app/src/test/resources/sql/db-test-data.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ INSERT INTO account (id, email, currency, money_amount) VALUES
1111
(DEFAULT, '[email protected]', 'USD', 573),
1212
(DEFAULT, '[email protected]', 'USD', 79),
1313
(DEFAULT, '[email protected]', 'USD', 33),
14-
(DEFAULT, '[email protected]', 'USD', 100);
14+
(DEFAULT, '[email protected]', 'USD', 100),
15+
(DEFAULT, '[email protected]', 'GBP', 80);

settings.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
rootProject.name = 'java-microservices'
22
include 'common-api'
3+
include 'common-wiremock'
34
include 'spring-app'
45
include 'dropwizard-app'
56
include 'loadtest'

spring-app/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ dependencies {
2626
runtimeOnly 'org.postgresql:postgresql:42.4.0'
2727

2828
testImplementation 'org.springframework.boot:spring-boot-starter-test'
29+
testImplementation project(":common-wiremock")
30+
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock:3.1.3'
2931
testImplementation 'org.testcontainers:testcontainers:1.17.3'
3032
testImplementation 'org.testcontainers:junit-jupiter:1.17.3'
3133
testImplementation 'org.testcontainers:postgresql:1.17.3'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package bitxon.spring.client;
2+
3+
import bitxon.api.thirdparty.exchange.model.ExchangeRate;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.client.RestTemplate;
7+
8+
@Component
9+
@RequiredArgsConstructor
10+
public class ExchangeClient {
11+
12+
private final RestTemplate restTemplate;
13+
private final ExchangeClientProperties exchangeClientProperties;
14+
15+
16+
public ExchangeRate getExchangeRate(String currency) {
17+
var response = restTemplate.getForObject(
18+
exchangeClientProperties.getBasePath() + currency,
19+
ExchangeRate.class
20+
);
21+
return response;
22+
}
23+
}

0 commit comments

Comments
 (0)