diff --git a/java-spi/exchange-rate-api/pom.xml b/java-spi/exchange-rate-api/pom.xml
new file mode 100644
index 0000000000..27651533a9
--- /dev/null
+++ b/java-spi/exchange-rate-api/pom.xml
@@ -0,0 +1,14 @@
+
+ 4.0.0
+
+ exchange-rate-api
+ jar
+
+
+ com.baeldung
+ java-spi
+ 1.0.0-SNAPSHOT
+
+
+
diff --git a/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/ExchangeRate.java b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/ExchangeRate.java
new file mode 100644
index 0000000000..afc7ef92ce
--- /dev/null
+++ b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/ExchangeRate.java
@@ -0,0 +1,42 @@
+package com.baeldung.rate;
+
+import com.baeldung.rate.exception.ProviderNotFoundException;
+import com.baeldung.rate.spi.ExchangeRateProvider;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+
+public final class ExchangeRate {
+
+ private static final String DEFAULT_PROVIDER = "com.baeldung.rate.spi.YahooFinanceExchangeRateProvider";
+
+ //All providers
+ public static List providers() {
+ List services = new ArrayList<>();
+ ServiceLoader loader = ServiceLoader.load(ExchangeRateProvider.class);
+ loader.forEach(exchangeRateProvider -> {
+ services.add(exchangeRateProvider);
+ });
+ return services;
+ }
+
+ //Default provider
+ public static ExchangeRateProvider provider() {
+ return provider(DEFAULT_PROVIDER);
+ }
+
+ //provider by name
+ public static ExchangeRateProvider provider(String providerName) {
+ ServiceLoader loader = ServiceLoader.load(ExchangeRateProvider.class);
+ Iterator it = loader.iterator();
+ while (it.hasNext()) {
+ ExchangeRateProvider provider = it.next();
+ if (providerName.equals(provider.getClass().getName())) {
+ return provider;
+ }
+ }
+ throw new ProviderNotFoundException("Exchange Rate provider " + providerName + " not found");
+ }
+}
diff --git a/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/Quote.java b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/Quote.java
new file mode 100644
index 0000000000..577af3b618
--- /dev/null
+++ b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/Quote.java
@@ -0,0 +1,44 @@
+package com.baeldung.rate.api;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class Quote {
+ private String currency;
+ private BigDecimal ask;
+ private BigDecimal bid;
+ private LocalDate date;
+ //...
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+
+ public BigDecimal getAsk() {
+ return ask;
+ }
+
+ public void setAsk(BigDecimal ask) {
+ this.ask = ask;
+ }
+
+ public BigDecimal getBid() {
+ return bid;
+ }
+
+ public void setBid(BigDecimal bid) {
+ this.bid = bid;
+ }
+
+ public LocalDate getDate() {
+ return date;
+ }
+
+ public void setDate(LocalDate date) {
+ this.date = date;
+ }
+}
\ No newline at end of file
diff --git a/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/QuoteManager.java b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/QuoteManager.java
new file mode 100644
index 0000000000..16416eaf65
--- /dev/null
+++ b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/api/QuoteManager.java
@@ -0,0 +1,8 @@
+package com.baeldung.rate.api;
+
+import java.time.LocalDate;
+import java.util.List;
+
+public interface QuoteManager {
+ List getQuotes(String baseCurrency, LocalDate date);
+}
diff --git a/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/exception/ProviderNotFoundException.java b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/exception/ProviderNotFoundException.java
new file mode 100644
index 0000000000..3a2d92c4fd
--- /dev/null
+++ b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/exception/ProviderNotFoundException.java
@@ -0,0 +1,13 @@
+package com.baeldung.rate.exception;
+
+public class ProviderNotFoundException extends RuntimeException {
+
+ public ProviderNotFoundException() {
+ super();
+ }
+
+ public ProviderNotFoundException(String message) {
+ super(message);
+ }
+
+}
diff --git a/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/spi/ExchangeRateProvider.java b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/spi/ExchangeRateProvider.java
new file mode 100644
index 0000000000..c3157b2b03
--- /dev/null
+++ b/java-spi/exchange-rate-api/src/main/java/com/baeldung/rate/spi/ExchangeRateProvider.java
@@ -0,0 +1,7 @@
+package com.baeldung.rate.spi;
+
+import com.baeldung.rate.api.QuoteManager;
+
+public interface ExchangeRateProvider {
+ QuoteManager create();
+}
\ No newline at end of file
diff --git a/java-spi/exchange-rate-app/pom.xml b/java-spi/exchange-rate-app/pom.xml
new file mode 100644
index 0000000000..7e64cf7438
--- /dev/null
+++ b/java-spi/exchange-rate-app/pom.xml
@@ -0,0 +1,27 @@
+
+ 4.0.0
+
+ exchange-rate-app
+ jar
+
+
+ com.baeldung
+ java-spi
+ 1.0.0-SNAPSHOT
+
+
+
+
+ com.baeldung
+ exchange-rate-api
+ 1.0.0-SNAPSHOT
+
+
+ com.baeldung
+ exchange-rate-impl
+ 1.0.0-SNAPSHOT
+
+
+
+
diff --git a/java-spi/exchange-rate-app/src/main/java/com.baeldung.rate.app/MainApp.java b/java-spi/exchange-rate-app/src/main/java/com.baeldung.rate.app/MainApp.java
new file mode 100644
index 0000000000..fd43ed3a85
--- /dev/null
+++ b/java-spi/exchange-rate-app/src/main/java/com.baeldung.rate.app/MainApp.java
@@ -0,0 +1,21 @@
+package com.baeldung.rate.app;
+
+import com.baeldung.rate.ExchangeRate;
+import com.baeldung.rate.api.Quote;
+
+import java.time.LocalDate;
+import java.util.List;
+
+public class MainApp {
+ public static void main(String... args) {
+ ExchangeRate.providers().forEach(provider -> {
+ System.out.println("Retreiving USD quotes from provider :" + provider);
+ List quotes = provider.create().getQuotes("USD", LocalDate.now());
+ System.out.println(String.format("%14s%12s|%12s", "","Ask", "Bid"));
+ System.out.println("----------------------------------------");
+ quotes.forEach(quote -> {
+ System.out.println("USD --> " + quote.getCurrency() + " : " + String.format("%12f|%12f", quote.getAsk(), quote.getBid()));
+ });
+ });
+ }
+}
diff --git a/java-spi/exchange-rate-impl/pom.xml b/java-spi/exchange-rate-impl/pom.xml
new file mode 100644
index 0000000000..ec22791351
--- /dev/null
+++ b/java-spi/exchange-rate-impl/pom.xml
@@ -0,0 +1,42 @@
+
+ 4.0.0
+
+ exchange-rate-impl
+ jar
+
+
+ com.baeldung
+ java-spi
+ 1.0.0-SNAPSHOT
+
+
+
+
+ com.baeldung
+ exchange-rate-api
+ 1.0.0-SNAPSHOT
+
+
+ com.squareup.okhttp3
+ okhttp
+ 3.10.0
+
+
+ javax.json.bind
+ javax.json.bind-api
+ 1.0
+
+
+ org.eclipse
+ yasson
+ 1.0.1
+
+
+ org.glassfish
+ javax.json
+ 1.1.2
+
+
+
+
diff --git a/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponse.java b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponse.java
new file mode 100644
index 0000000000..9ba4fb26b0
--- /dev/null
+++ b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponse.java
@@ -0,0 +1,26 @@
+package com.baeldung.rate.impl;
+
+import com.baeldung.rate.api.Quote;
+
+import java.util.List;
+
+public class QuoteResponse {
+ private List result;
+ private String error;
+
+ public List getResult() {
+ return result;
+ }
+
+ public void setResult(List result) {
+ this.result = result;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+}
diff --git a/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponseWrapper.java b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponseWrapper.java
new file mode 100644
index 0000000000..6d7be086f0
--- /dev/null
+++ b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/QuoteResponseWrapper.java
@@ -0,0 +1,13 @@
+package com.baeldung.rate.impl;
+
+public class QuoteResponseWrapper {
+ private QuoteResponse quoteResponse;
+
+ public QuoteResponse getQuoteResponse() {
+ return quoteResponse;
+ }
+
+ public void setQuoteResponse(QuoteResponse quoteResponse) {
+ this.quoteResponse = quoteResponse;
+ }
+}
diff --git a/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooFinanceExchangeRateProvider.java b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooFinanceExchangeRateProvider.java
new file mode 100644
index 0000000000..9a069cfde4
--- /dev/null
+++ b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooFinanceExchangeRateProvider.java
@@ -0,0 +1,13 @@
+package com.baeldung.rate.impl;
+
+import com.baeldung.rate.api.QuoteManager;
+import com.baeldung.rate.spi.ExchangeRateProvider;
+
+public class YahooFinanceExchangeRateProvider implements ExchangeRateProvider {
+
+ @Override
+ public QuoteManager create() {
+ return new YahooQuoteManagerImpl();
+ }
+
+}
\ No newline at end of file
diff --git a/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooQuoteManagerImpl.java b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooQuoteManagerImpl.java
new file mode 100644
index 0000000000..8cc68259be
--- /dev/null
+++ b/java-spi/exchange-rate-impl/src/main/java/com/baeldung/rate/impl/YahooQuoteManagerImpl.java
@@ -0,0 +1,65 @@
+package com.baeldung.rate.impl;
+
+import com.baeldung.rate.api.Quote;
+import com.baeldung.rate.api.QuoteManager;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import javax.json.bind.JsonbBuilder;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.time.LocalDate;
+import java.util.Currency;
+import java.util.List;
+
+public class YahooQuoteManagerImpl implements QuoteManager {
+
+ static final String URL_PROVIDER = "https://query1.finance.yahoo.com/v7/finance/quote";
+ OkHttpClient client = new OkHttpClient();
+
+ @Override
+ public List getQuotes(String baseCurrency, LocalDate date) {
+
+ StringBuilder sb = new StringBuilder();
+ Currency.getAvailableCurrencies().forEach(currency -> {
+ if (!currency.equals(currency.getCurrencyCode())) {
+ sb.append(baseCurrency).append(currency.getCurrencyCode()).append("=X").append(",");
+ }
+ });
+
+ String value = "";
+ try {
+ value = URLEncoder.encode(sb.toString().substring(0, sb.toString().length() - 1), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ String queryString = String.format("%s=%s", "symbols", value);
+ String response = doGetRequest(queryString);
+ System.out.println(response);
+ return map(response);
+ }
+
+ private List map(String response) {
+ QuoteResponseWrapper qrw = JsonbBuilder.create().fromJson(response, QuoteResponseWrapper.class);
+ return qrw.getQuoteResponse().getResult();
+ }
+
+ String doGetRequest(String queryString) {
+ String fullUrl = URL_PROVIDER + "?" + queryString;
+
+ System.out.println(fullUrl);
+ Request request = new Request.Builder()
+ .url(fullUrl)
+ .build();
+ Response response = null;
+ try {
+ response = client.newCall(request).execute();
+ return response.body().string();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+}
diff --git a/java-spi/exchange-rate-impl/src/main/resources/META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider b/java-spi/exchange-rate-impl/src/main/resources/META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider
new file mode 100644
index 0000000000..67ba8a8227
--- /dev/null
+++ b/java-spi/exchange-rate-impl/src/main/resources/META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider
@@ -0,0 +1 @@
+com.baeldung.rate.impl.YahooFinanceExchangeRateProvider
\ No newline at end of file
diff --git a/java-spi/pom.xml b/java-spi/pom.xml
new file mode 100644
index 0000000000..8eaa184d8d
--- /dev/null
+++ b/java-spi/pom.xml
@@ -0,0 +1,20 @@
+
+ 4.0.0
+
+ java-spi
+ pom
+
+
+ com.baeldung
+ parent-modules
+ 1.0.0-SNAPSHOT
+
+
+
+ exchange-rate-api
+ exchange-rate-impl
+ exchange-rate-app
+
+
+
diff --git a/pom.xml b/pom.xml
index 3092c0fa40..0c72a82d2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -278,6 +278,7 @@
lucene
vraptor
persistence-modules/java-cockroachdb
+ java-spi