From 6cff93045d285cf5ed30ef676a3d9e66c11d484f Mon Sep 17 00:00:00 2001
From: Henry Borasch <Henry.Borasch@sinc.de>
Date: Mon, 17 Apr 2023 10:08:17 +0200
Subject: [PATCH] 978: removed spring-web

- removed dependency
- added OkHttpClient
- created intermediate client to abstract between logic and HTTP handling
- adjusted tests
---
 api/pom.xml                                   |   4 -
 .../fitconnect/api/config/ResourcePaths.java  |  10 +-
 .../api/exceptions/RestApiException.java      |  19 ++
 .../client/factory/ClientFactory.java         |  75 +++---
 .../client/subscriber/SubmissionReceiver.java |   3 +-
 .../client/SubscriberClientTest.java          |   9 +-
 core/pom.xml                                  |   9 +-
 .../core/auth/DefaultOAuthService.java        |  42 ++--
 .../core/events/EventLogApiService.java       |  47 ++--
 .../core/http/ApiRequestInterceptor.java      |  21 +-
 .../fitconnect/core/http/HttpClient.java      | 115 +++++++++
 .../fitconnect/core/http/HttpHeaders.java     |  10 +
 .../fitconnect/core/http/HttpResponse.java    |  11 +
 .../fitko/fitconnect/core/http/MimeTypes.java |   9 +
 .../fitconnect/core/http/RestService.java     |  50 +---
 .../core/http/UserAgentInterceptor.java       |  23 +-
 .../core/keys/PublicKeyService.java           |  45 ++--
 .../core/routing/RoutingApiService.java       |  72 +++---
 .../core/schema/SchemaResourceProvider.java   |  10 +-
 .../core/submission/SubmissionApiService.java | 229 ++++++------------
 .../core/auth/DefaultOAuthServiceTest.java    |   8 +-
 .../core/events/EventLogApiServiceTest.java   |  18 +-
 .../events/SecurityEventTokenServiceTest.java |   4 +-
 .../core/http/ProxyRestTemplateTest.java      |  10 +-
 .../fitconnect/core/http/RestServiceTest.java |  10 +-
 .../core/http/RestTemplateTest.java           |  44 +---
 .../core/keys/PublicKeyServiceTest.java       |  24 +-
 .../core/routing/RoutingApiServiceTest.java   |   8 +-
 .../schema/SchemaResourceProviderTest.java    |  39 +--
 .../submission/SubmissionApiServiceTest.java  |  30 ++-
 .../DefaultValidationServiceTest.java         |   6 +-
 .../integrationtests/AuthenticationIT.java    |   2 +-
 pom.xml                                       |   6 -
 33 files changed, 508 insertions(+), 514 deletions(-)
 create mode 100644 core/src/main/java/dev/fitko/fitconnect/core/http/HttpClient.java
 create mode 100644 core/src/main/java/dev/fitko/fitconnect/core/http/HttpHeaders.java
 create mode 100644 core/src/main/java/dev/fitko/fitconnect/core/http/HttpResponse.java
 create mode 100644 core/src/main/java/dev/fitko/fitconnect/core/http/MimeTypes.java

diff --git a/api/pom.xml b/api/pom.xml
index 60bee28c9..e8fee6738 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -30,10 +30,6 @@
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-web</artifactId>
-        </dependency>
     </dependencies>
 
     <build>
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
index 0f429301b..367aee5f1 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
@@ -7,14 +7,14 @@ final class ResourcePaths {
 
     static final String AUTH_TOKEN_PATH = "/token";
 
-    static final String DESTINATIONS_PATH = "/v1/destinations/{destinationId}";
-    static final String DESTINATIONS_KEY_PATH = "/v1/destinations/{destinationId}/keys/{kid}";
+    static final String DESTINATIONS_PATH = "/v1/destinations/%s";
+    static final String DESTINATIONS_KEY_PATH = "/v1/destinations/%s/keys/%s";
 
-    static final String EVENTS_PATH = "/v1/cases/{caseId}/events";
+    static final String EVENTS_PATH = "/v1/cases/%s/events";
 
-    static final String SUBMISSION_PATH = "/v1/submissions/{submissionId}";
+    static final String SUBMISSION_PATH = "/v1/submissions/%s";
     static final String SUBMISSIONS_PATH = "/v1/submissions";
-    static final String SUBMISSION_ATTACHMENT_PATH = "/v1/submissions/{submissionId}/attachments/{attachmentId}";
+    static final String SUBMISSION_ATTACHMENT_PATH = "/v1/submissions/%s/attachments/%s";
 
     static final String ROUTING_AREA_PATH = "/v1/areas";
     static final String ROUTING_ROUTE_PATH = "/v1/routes";
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java
index 6fdfc7cd3..ef121c601 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java
@@ -2,11 +2,30 @@ package dev.fitko.fitconnect.api.exceptions;
 
 public class RestApiException extends RuntimeException {
 
+    private int statusCode = 400;
+
     public RestApiException(final String errorMessage, final Throwable error) {
         super(errorMessage, error);
     }
 
+    public RestApiException(final String errorMessage, final Throwable error, final int statusCode) {
+        super(errorMessage, error);
+        this.statusCode = statusCode;
+    }
+
     public RestApiException(final String errorMessage) {
         super(errorMessage);
     }
+
+    public RestApiException(final String errorMessage, int statusCode) {
+        super(errorMessage);
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+
+    public boolean isNotFound() {
+        return this.statusCode == 404;
+    }
 }
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
index 849f8bc39..856c8c6a3 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
@@ -34,6 +34,7 @@ import dev.fitko.fitconnect.core.crypto.JWECryptoService;
 import dev.fitko.fitconnect.core.events.EventLogApiService;
 import dev.fitko.fitconnect.core.events.EventLogVerifier;
 import dev.fitko.fitconnect.core.events.SecurityEventTokenService;
+import dev.fitko.fitconnect.core.http.HttpClient;
 import dev.fitko.fitconnect.core.http.RestService;
 import dev.fitko.fitconnect.core.keys.PublicKeyService;
 import dev.fitko.fitconnect.core.routing.RouteVerifier;
@@ -44,7 +45,6 @@ import dev.fitko.fitconnect.core.util.CertificateLoader;
 import dev.fitko.fitconnect.core.validation.DefaultValidationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.client.RestTemplate;
 import org.yaml.snakeyaml.Yaml;
 
 import java.io.IOException;
@@ -102,6 +102,7 @@ public final class ClientFactory {
 
         return new SubscriberClient(subscriber, submissionReceiver);
     }
+
     /**
      * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
      *
@@ -112,74 +113,74 @@ public final class ClientFactory {
         checkNotNull(config);
 
         LOGGER.info("Initializing routing client ...");
-        final RestTemplate restTemplate = getRestTemplate(config, loadBuildInfo());
-        final SchemaProvider schemaProvider = getSchemaProvider(restTemplate, config.getSubmissionDataSchemaPath());
-        final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
+        final HttpClient httpClient = getHttpClient(config, loadBuildInfo());
+        final SchemaProvider schemaProvider = getSchemaProvider(httpClient, config.getSubmissionDataSchemaPath());
+        final OAuthService authService = getSenderConfiguredAuthService(config, httpClient);
 
         final MessageDigestService messageDigestService = getMessageDigestService();
         final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
-        final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
-        final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
+        final SubmissionService submissionService = getSubmissionService(config, httpClient, authService);
+        final KeyService keyService = getKeyService(config, httpClient, authService, submissionService, validator);
 
         final RouteVerifier routeVerifier = getRouteVerifier(keyService, validator);
-        final RoutingService routingService = getRoutingService(config, restTemplate);
+        final RoutingService routingService = getRoutingService(config, httpClient);
 
         return new RoutingClient(routingService, routeVerifier);
     }
 
     private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
-        final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
-        final SchemaProvider schemaProvider = getSchemaProvider(restTemplate, config.getSubmissionDataSchemaPath());
+        final HttpClient httpClient = getHttpClient(config, buildInfo);
+        final SchemaProvider schemaProvider = getSchemaProvider(httpClient, config.getSubmissionDataSchemaPath());
         final MessageDigestService messageDigestService = getMessageDigestService();
 
         final CryptoService cryptoService = getCryptoService(messageDigestService);
         final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
-        final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
+        final OAuthService authService = getSenderConfiguredAuthService(config, httpClient);
 
-        final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
-        final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
+        final SubmissionService submissionService = getSubmissionService(config, httpClient, authService);
+        final KeyService keyService = getKeyService(config, httpClient, authService, submissionService, validator);
         final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
-        final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
+        final EventLogService eventLogService = getEventLogService(config, httpClient, eventLogVerifier, authService);
 
         return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService);
     }
 
     private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
-        final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
-        final SchemaProvider schemaProvider = getSchemaProvider(restTemplate, config.getSubmissionDataSchemaPath());
+        final HttpClient httpClient = getHttpClient(config, buildInfo);
+        final SchemaProvider schemaProvider = getSchemaProvider(httpClient, config.getSubmissionDataSchemaPath());
         final MessageDigestService messageDigestService = getMessageDigestService();
 
         final CryptoService cryptoService = getCryptoService(messageDigestService);
         final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
-        final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate);
+        final OAuthService authService = getSubscriberConfiguredAuthService(config, httpClient);
 
-        final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
-        final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
+        final SubmissionService submissionService = getSubmissionService(config, httpClient, authService);
+        final KeyService keyService = getKeyService(config, httpClient, authService, submissionService, validator);
         final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
-        final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
+        final EventLogService eventLogService = getEventLogService(config, httpClient, eventLogVerifier, authService);
         final SecurityEventService setService = getSecurityEventTokenService(config, validator);
 
         return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
     }
 
-    private static OAuthService getSenderConfiguredAuthService(final ApplicationConfig config, final RestTemplate restTemplate) {
+    private static OAuthService getSenderConfiguredAuthService(final ApplicationConfig config, final HttpClient httpClient) {
         final String clientId = config.getSenderConfig().getClientId();
         final String clientSecret = config.getSenderConfig().getClientSecret();
-        return new DefaultOAuthService(restTemplate, clientId, clientSecret, config.getOAuthTokenEndpoint());
+        return new DefaultOAuthService(httpClient, clientId, clientSecret, config.getOAuthTokenEndpoint());
     }
 
-    private static OAuthService getSubscriberConfiguredAuthService(final ApplicationConfig config, final RestTemplate restTemplate) {
+    private static OAuthService getSubscriberConfiguredAuthService(final ApplicationConfig config, final HttpClient httpClient) {
         final String clientId = config.getSubscriberConfig().getClientId();
         final String clientSecret = config.getSubscriberConfig().getClientSecret();
-        return new DefaultOAuthService(restTemplate, clientId, clientSecret, config.getOAuthTokenEndpoint());
+        return new DefaultOAuthService(httpClient, clientId, clientSecret, config.getOAuthTokenEndpoint());
     }
 
-    private static SubmissionService getSubmissionService(final ApplicationConfig config, final RestTemplate restTemplate, final OAuthService authService) {
-        return new SubmissionApiService(authService, restTemplate, config);
+    private static SubmissionService getSubmissionService(final ApplicationConfig config, final HttpClient httpClient, final OAuthService authService) {
+        return new SubmissionApiService(authService, httpClient, config);
     }
 
-    private static EventLogService getEventLogService(final ApplicationConfig config, final RestTemplate restTemplate, final EventLogVerificationService eventLogVerifier, final OAuthService authService) {
-        return new EventLogApiService(config, authService, restTemplate, eventLogVerifier);
+    private static EventLogService getEventLogService(final ApplicationConfig config, final HttpClient httpClient, final EventLogVerificationService eventLogVerifier, final OAuthService authService) {
+        return new EventLogApiService(config, authService, httpClient, eventLogVerifier);
     }
 
     private static ValidationService getValidationService(final ApplicationConfig config, final SchemaProvider schemaProvider, final MessageDigestService messageDigestService) {
@@ -190,9 +191,9 @@ public final class ClientFactory {
         return new JWECryptoService(messageDigestService);
     }
 
-    private static RestTemplate getRestTemplate(final ApplicationConfig config, final BuildInfo buildInfo) {
+    private static HttpClient getHttpClient(final ApplicationConfig config, final BuildInfo buildInfo) {
         final RestService restService = new RestService(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
-        return restService.getRestTemplate();
+        return restService.getHttpClient();
     }
 
     private static MessageDigestService getMessageDigestService() {
@@ -206,24 +207,24 @@ public final class ClientFactory {
         return new SecurityEventTokenService(config, validationService, rsaKey);
     }
 
-    private static KeyService getKeyService(final ApplicationConfig config, final RestTemplate restTemplate, final OAuthService authService, final SubmissionService submissionService, final ValidationService validator) {
-        return new PublicKeyService(config, restTemplate, authService, submissionService, validator);
+    private static KeyService getKeyService(final ApplicationConfig config, final HttpClient httpClient, final OAuthService authService, final SubmissionService submissionService, final ValidationService validator) {
+        return new PublicKeyService(config, httpClient, authService, submissionService, validator);
     }
 
     private static EventLogVerificationService getEventLogVerifier(final KeyService keyService, final ValidationService validationService) {
         return new EventLogVerifier(keyService, validationService);
     }
 
-    private static SchemaProvider getSchemaProvider(RestTemplate restTemplate, String submissionDataSchemaPath) {
+    private static SchemaProvider getSchemaProvider(HttpClient httpClient, String submissionDataSchemaPath) {
         final List<String> setSchemaFiles = SchemaConfig.getSetSchemaFilePaths(SET_SCHEMA_DIR);
         final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR);
         final List<String> destinationSchemaFiles = SchemaConfig.getDestinationSchemaPaths(DESTINATION_SCHEMA_DIR);
         final SchemaResources schemaResources = new SchemaResources(setSchemaFiles, metadataSchemaFiles, destinationSchemaFiles, submissionDataSchemaPath);
-        return new SchemaResourceProvider(restTemplate, schemaResources);
+        return new SchemaResourceProvider(httpClient, schemaResources);
     }
 
-    private static RoutingService getRoutingService(final ApplicationConfig config, final RestTemplate restTemplate) {
-        return new RoutingApiService(config, restTemplate);
+    private static RoutingService getRoutingService(final ApplicationConfig config, final HttpClient httpClient) {
+        return new RoutingApiService(config, httpClient);
     }
 
     private static RouteVerifier getRouteVerifier(final KeyService keyService, final ValidationService validationService) {
@@ -254,13 +255,13 @@ public final class ClientFactory {
     }
 
     private static void checkNotNull(final ApplicationConfig config) {
-        if(config == null){
+        if (config == null) {
             throw new InitializationException("application config must not be null");
         }
     }
 
     private static String getPrivateDecryptionKeyPathFromSubscriber(final SubscriberConfig subscriberConfig) {
-        if(subscriberConfig.getPrivateDecryptionKeyPaths().size() != 1){
+        if (subscriberConfig.getPrivateDecryptionKeyPaths().size() != 1) {
             throw new InitializationException("Currently only one configured private key per subscriber is allowed !");
         }
         final String keyPath = subscriberConfig.getPrivateDecryptionKeyPaths().get(0);
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java b/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java
index 14a3385d9..ab00bfff2 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java
@@ -34,7 +34,6 @@ import dev.fitko.fitconnect.client.subscriber.model.ReceivedData;
 import dev.fitko.fitconnect.core.util.StopWatch;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.client.HttpClientErrorException;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -193,7 +192,7 @@ public class SubmissionReceiver {
             LOGGER.info("Downloading attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDownload));
             return encryptedAttachment;
         } catch (final RestApiException e) {
-            if (e.getCause() instanceof HttpClientErrorException.NotFound) {
+            if (e.isNotFound()) {
                 rejectSubmissionWithProblem(submission, new MissingAttachment(metadata.getAttachmentId()));
             }
             throw new SubmissionRequestException(e.getMessage(), e);
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java
index c8deaf7d0..ad116a730 100644
--- a/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java
+++ b/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java
@@ -43,8 +43,6 @@ import dev.fitko.fitconnect.core.crypto.JWECryptoService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.client.HttpClientErrorException;
 
 import java.io.File;
 import java.io.IOException;
@@ -62,7 +60,10 @@ import java.util.UUID;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.anyLong;
@@ -730,7 +731,7 @@ class SubscriberClientTest {
         when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes());
         when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes);
         when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags);
-        when(subscriberMock.fetchAttachment(any(), any())).thenThrow(new RestApiException("Attachment download failed", HttpClientErrorException.create(HttpStatus.NOT_FOUND, "attachment not found", null, null, null)));
+        when(subscriberMock.fetchAttachment(any(), any())).thenThrow(new RestApiException("Attachment download failed", 404));
 
         // When
         final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId));
diff --git a/core/pom.xml b/core/pom.xml
index 3ed1acb16..44b2e3b05 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -30,10 +30,6 @@
             <groupId>com.nimbusds</groupId>
             <artifactId>nimbus-jose-jwt</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-web</artifactId>
-        </dependency>
         <dependency>
             <groupId>com.networknt</groupId>
             <artifactId>json-schema-validator</artifactId>
@@ -51,6 +47,11 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.10.0</version>
+        </dependency>
 
         <!-- Tests -->
         <dependency>
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
index 908d0b2ce..d84d38fa1 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
@@ -4,28 +4,28 @@ import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
 import dev.fitko.fitconnect.api.exceptions.AuthenticationException;
 import dev.fitko.fitconnect.api.exceptions.RestApiException;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
+import dev.fitko.fitconnect.core.http.HttpClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
+import java.util.Map;
 
+import static dev.fitko.fitconnect.core.http.HttpHeaders.ACCEPT;
+import static dev.fitko.fitconnect.core.http.HttpHeaders.ACCEPT_CHARSET;
+import static dev.fitko.fitconnect.core.http.HttpHeaders.CONTENT_TYPE;
+import static dev.fitko.fitconnect.core.http.MimeTypes.APPLICATION_FORM_URLENCODED;
+import static dev.fitko.fitconnect.core.http.MimeTypes.APPLICATION_JSON;
 import static java.util.stream.Collectors.joining;
 
 public class DefaultOAuthService implements OAuthService {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuthService.class);
 
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
 
     private final String tokenUrl;
     private final String clientId;
@@ -34,8 +34,8 @@ public class DefaultOAuthService implements OAuthService {
     private OAuthToken currentToken;
     private LocalDateTime tokenExpirationTime;
 
-    public DefaultOAuthService(final RestTemplate restTemplate, final String clientId, final String clientSecret, final String authUrl) {
-        this.restTemplate = restTemplate;
+    public DefaultOAuthService(final HttpClient httpClient, final String clientId, final String clientSecret, final String authUrl) {
+        this.httpClient = httpClient;
         this.clientId = clientId;
         this.clientSecret = clientSecret;
         tokenUrl = authUrl;
@@ -91,23 +91,21 @@ public class DefaultOAuthService implements OAuthService {
     }
 
     private OAuthToken performTokenRequest(final String requestBody) throws RestApiException {
-        final HttpHeaders headers = getHeaders();
-        final HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
+
         try {
             LOGGER.info("Sending authentication request");
-            return restTemplate.exchange(tokenUrl, HttpMethod.POST, entity, OAuthToken.class).getBody();
-        } catch (final RestClientException e) {
+            return this.httpClient.post(tokenUrl, getHeaders(), requestBody, OAuthToken.class).getBody();
+        } catch (final IOException e) {
             LOGGER.error(e.getMessage(), e);
             throw new RestApiException("Could not retrieve OAuth token", e);
         }
     }
 
-    private HttpHeaders getHeaders() {
-        final var headers = new HttpHeaders();
-        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
-        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
-        return headers;
-    }
+    private Map<String, String> getHeaders() {
 
+        return new HashMap<>(Map.of(
+                ACCEPT, APPLICATION_JSON,
+                CONTENT_TYPE, APPLICATION_FORM_URLENCODED,
+                ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()));
+    }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java
index 7284f40a7..37faa6e5f 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java
@@ -17,19 +17,19 @@ import dev.fitko.fitconnect.api.exceptions.SubmitEventNotFoundException;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.events.EventLogService;
 import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.HttpHeaders;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import dev.fitko.fitconnect.core.util.EventLogUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -43,20 +43,19 @@ import static dev.fitko.fitconnect.core.util.EventLogUtil.mapEventLogToEntries;
 public class EventLogApiService implements EventLogService {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(EventLogApiService.class);
-    public static final MediaType MEDIA_TYPE_JOSE = MediaType.parseMediaType("application/jose");
 
     private final ApplicationConfig config;
     private final OAuthService authService;
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
     private final EventLogVerificationService eventLogVerifier;
 
     public EventLogApiService(final ApplicationConfig config,
                               final OAuthService authService,
-                              final RestTemplate restTemplate,
+                              final HttpClient httpClient,
                               final EventLogVerificationService eventLogVerifier) {
         this.config = config;
         this.authService = authService;
-        this.restTemplate = restTemplate;
+        this.httpClient = httpClient;
         this.eventLogVerifier = eventLogVerifier;
     }
 
@@ -111,33 +110,29 @@ public class EventLogApiService implements EventLogService {
 
     @Override
     public void sendEvent(final UUID caseId, final String signedAndSerializedSET) {
-        final String url = config.getEventsEndpoint();
-        final HttpHeaders headers = getHttpHeaders(MEDIA_TYPE_JOSE);
-        final HttpEntity<String> entity = new HttpEntity<>(signedAndSerializedSET, headers);
+        final String url = String.format(config.getEventsEndpoint(), caseId);
         try {
-            restTemplate.exchange(url, HttpMethod.POST, entity, Void.class, caseId);
-        } catch (final RestClientException e) {
+            this.httpClient.post(url, getHttpHeaders(MimeTypes.APPLICATION_JOSE), signedAndSerializedSET, Void.class);
+        } catch (final IOException e) {
             throw new RestApiException("Sending event failed", e);
         }
     }
 
     private EventLog loadEventLog(final UUID caseId) {
-        final String url = config.getEventsEndpoint();
-        final HttpHeaders headers = getHttpHeaders(MediaType.APPLICATION_JSON);
-        final HttpEntity<String> entity = new HttpEntity<>(headers);
+        final String url = String.format(config.getEventsEndpoint(), caseId);
         try {
-            return restTemplate.exchange(url, HttpMethod.GET, entity, EventLog.class, caseId).getBody();
-        } catch (final RestClientException e) {
+            return this.httpClient.get(url, getHttpHeaders(MimeTypes.APPLICATION_JSON), EventLog.class).getBody();
+        } catch (final IOException e) {
             throw new EventLogException("EventLog query failed", e);
         }
     }
 
-    private HttpHeaders getHttpHeaders(final MediaType mediaType) {
-        final var headers = new HttpHeaders();
-        headers.setBearerAuth(authService.getCurrentToken().getAccessToken());
-        headers.setContentType(mediaType);
-        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
-        return headers;
+    private Map<String, String> getHttpHeaders(final String mediaType) {
+
+        return new HashMap<>(Map.of(
+                HttpHeaders.AUTHORIZATION, "Bearer " + authService.getCurrentToken().getAccessToken(),
+                HttpHeaders.CONTENT_TYPE, mediaType,
+                HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()));
     }
 
     private SubmissionStatus getVerifiedStatus(final ValidationContext validationContext, final SignedJWT latestEvent) {
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/ApiRequestInterceptor.java b/core/src/main/java/dev/fitko/fitconnect/core/http/ApiRequestInterceptor.java
index d4bbd501e..1cf88cfb5 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/ApiRequestInterceptor.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/ApiRequestInterceptor.java
@@ -1,25 +1,28 @@
 package dev.fitko.fitconnect.core.http;
 
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpRequestExecution;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.ClientHttpResponse;
 
 import java.io.IOException;
 
 /**
  * Intercepts HTTP calls and logs the request url
  */
-public class ApiRequestInterceptor implements ClientHttpRequestInterceptor {
+public class ApiRequestInterceptor implements Interceptor {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ApiRequestInterceptor.class);
 
+    @NotNull
     @Override
-    public ClientHttpResponse intercept(final HttpRequest req, final byte[] reqBody,
-                                        final ClientHttpRequestExecution ex) throws IOException {
-        LOGGER.info("{}", req.getURI());
-        return ex.execute(req, reqBody);
+    public Response intercept(@NotNull Chain chain) throws IOException {
+
+        Request request = chain.request();
+        LOGGER.info("{}", request.url());
+
+        return chain.proceed(request);
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/HttpClient.java b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpClient.java
new file mode 100644
index 000000000..ebf7f4c3b
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpClient.java
@@ -0,0 +1,115 @@
+package dev.fitko.fitconnect.core.http;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+import okhttp3.Headers;
+import okhttp3.Interceptor;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.util.List;
+import java.util.Map;
+
+public class HttpClient {
+
+    private final OkHttpClient httpClient;
+    private final ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    private final boolean throwExceptionsOnFailure;
+
+    public HttpClient(boolean throwExceptionsOnFailure) {
+        this.httpClient = new OkHttpClient();
+        this.throwExceptionsOnFailure = throwExceptionsOnFailure;
+    }
+
+    public HttpClient(boolean throwExceptionsOnFailure, List<Interceptor> interceptors) {
+        this.httpClient = builderWithInterceptors(interceptors).build();
+        this.throwExceptionsOnFailure = throwExceptionsOnFailure;
+    }
+
+    public HttpClient(boolean throwExceptionsOnFailure, List<Interceptor> interceptors, Proxy proxy) {
+        this.httpClient = builderWithInterceptors(interceptors).proxy(proxy).build();
+        this.throwExceptionsOnFailure = throwExceptionsOnFailure;
+    }
+
+    private OkHttpClient.Builder builderWithInterceptors(List<Interceptor> interceptors) {
+
+        OkHttpClient.Builder builder = new OkHttpClient.Builder();
+        interceptors.forEach(builder::addInterceptor);
+
+        return builder;
+    }
+
+    public <R> HttpResponse<R> get(String url, Class<R> responseType) throws IOException {
+        return get(url, Map.of(), responseType);
+    }
+
+    public <R> HttpResponse<R> get(String url, Map<String, String> headers, Class<R> responseType) throws IOException {
+
+        Request request = new Request.Builder()
+                .url(url)
+                .get()
+                .headers(Headers.of(headers))
+                .build();
+
+        try (Response response = this.httpClient.newCall(request).execute()) {
+            return this.evaluateStatusAndRespond(response, responseType);
+        }
+    }
+
+    public <P, R> HttpResponse<R> post(String url, P httpPayload, Class<R> responseType) throws IOException {
+        return post(url, Map.of(), httpPayload, responseType);
+    }
+
+    public <P, R> HttpResponse<R> post(String url, Map<String, String> headers, P httpPayload, Class<R> responseType) throws IOException {
+
+        RequestBody requestBody = RequestBody.create(this.objectMapper.writeValueAsString(httpPayload), MediaType.get(MimeTypes.APPLICATION_JSON));
+        Request request = new Request.Builder()
+                .url(url)
+                .post(requestBody)
+                .headers(Headers.of(headers))
+                .build();
+
+        try (Response response = this.httpClient.newCall(request).execute()) {
+            return this.evaluateStatusAndRespond(response, responseType);
+        }
+    }
+
+    public <P, R> HttpResponse<R> put(String url, P httpPayload, Class<R> responseType) throws IOException {
+        return put(url, Map.of(), httpPayload, responseType);
+    }
+
+    public <P, R> HttpResponse<R> put(String url, Map<String, String> headers, P httpPayload, Class<R> responseType) throws IOException {
+
+        RequestBody requestBody = RequestBody.create(this.objectMapper.writeValueAsString(httpPayload), MediaType.get(MimeTypes.APPLICATION_JSON));
+        Request request = new Request.Builder()
+                .url(url)
+                .put(requestBody)
+                .headers(Headers.of(headers))
+                .build();
+
+        try (Response response = this.httpClient.newCall(request).execute()) {
+            return this.evaluateStatusAndRespond(response, responseType);
+        }
+    }
+
+    private <R> HttpResponse<R> evaluateStatusAndRespond(Response response, Class<R> responseType) throws IOException {
+
+        if (this.throwExceptionsOnFailure && !response.isSuccessful()) {
+            throw new RestApiException("The Request failed.", response.code());
+        }
+
+        ResponseBody responseBody = response.body();
+        if (responseBody == null) {
+            throw new IOException("Response body is null.");
+        }
+
+        return new HttpResponse<R>(response.code(), this.objectMapper.readValue(responseBody.string(), responseType));
+    }
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/HttpHeaders.java b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpHeaders.java
new file mode 100644
index 000000000..23e41078f
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpHeaders.java
@@ -0,0 +1,10 @@
+package dev.fitko.fitconnect.core.http;
+
+public class HttpHeaders {
+
+    public final static String ACCEPT = "Accept";
+    public final static String ACCEPT_CHARSET = "Accept-Charset";
+    public final static String AUTHORIZATION = "Authorization";
+    public final static String CONTENT_TYPE = "Content-Type";
+    public final static String USER_AGENT = "User-Agent";
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/HttpResponse.java b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpResponse.java
new file mode 100644
index 000000000..fe4d08c1d
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/HttpResponse.java
@@ -0,0 +1,11 @@
+package dev.fitko.fitconnect.core.http;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class HttpResponse<T> {
+    private int statusCode;
+    private T body;
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/MimeTypes.java b/core/src/main/java/dev/fitko/fitconnect/core/http/MimeTypes.java
new file mode 100644
index 000000000..846fe5310
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/MimeTypes.java
@@ -0,0 +1,9 @@
+package dev.fitko.fitconnect.core.http;
+
+public class MimeTypes {
+
+    public final static String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
+    public final static String APPLICATION_JOSE = "application/jose";
+    public final static String APPLICATION_JSON = "application/json; charset=utf-8";
+    public final static String APPLICATION_PROBLEM_JSON = "application/problem+json";
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
index 7711d95ad..91663ff29 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
@@ -1,21 +1,14 @@
 package dev.fitko.fitconnect.core.http;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.config.BuildInfo;
 import dev.fitko.fitconnect.core.util.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.web.client.RestTemplate;
 
 import java.net.InetSocketAddress;
 import java.net.Proxy;
 import java.util.List;
 
-import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
-
 public class RestService {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(RestService.class);
@@ -34,44 +27,15 @@ public class RestService {
         this(null, 0, buildInfo);
     }
 
-    public RestTemplate getRestTemplate() {
-        return hasProxySet() ? getProxyRestTemplate() : getDefaultRestTemplate();
-    }
+    public HttpClient getHttpClient() {
 
-    private RestTemplate getDefaultRestTemplate() {
+        if (hasProxySet()) {
+            LOGGER.info("Using proxy {}", this);
+            return new HttpClient(true, List.of(new ApiRequestInterceptor(), new UserAgentInterceptor(buildInfo)),
+                    new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
+        }
         LOGGER.info("No proxy configured");
-        final RestTemplate restTemplate = new RestTemplate();
-        setupTemplate(restTemplate);
-        return restTemplate;
-    }
-
-    private RestTemplate getProxyRestTemplate() {
-        LOGGER.info("Using proxy {}", this);
-        final var requestFactory = new SimpleClientHttpRequestFactory();
-        final var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
-        requestFactory.setProxy(proxy);
-
-        final RestTemplate proxyRestTemplate = new RestTemplate(requestFactory);
-        setupTemplate(proxyRestTemplate);
-        return proxyRestTemplate;
-    }
-
-    private void setMappingConverters(final RestTemplate restTemplate) {
-        final MappingJackson2HttpMessageConverter jacksonMessageConverter = new MappingJackson2HttpMessageConverter();
-        jacksonMessageConverter.setObjectMapper(new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false));
-        restTemplate.getMessageConverters().add(jacksonMessageConverter);
-    }
-
-    private void setLoggingInterceptors(final RestTemplate restTemplate) {
-        final List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
-        interceptors.add(new ApiRequestInterceptor());
-        interceptors.add(new UserAgentInterceptor(buildInfo));
-        restTemplate.setInterceptors(interceptors);
-    }
-
-    private void setupTemplate(final RestTemplate restTemplate) {
-        setLoggingInterceptors(restTemplate);
-        setMappingConverters(restTemplate);
+        return new HttpClient(true, List.of(new ApiRequestInterceptor(), new UserAgentInterceptor(buildInfo)));
     }
 
     boolean hasProxySet() {
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/UserAgentInterceptor.java b/core/src/main/java/dev/fitko/fitconnect/core/http/UserAgentInterceptor.java
index c6d0d4e71..6aa9ec61c 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/UserAgentInterceptor.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/UserAgentInterceptor.java
@@ -1,15 +1,16 @@
 package dev.fitko.fitconnect.core.http;
 
 import dev.fitko.fitconnect.api.config.BuildInfo;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpRequestExecution;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.ClientHttpResponse;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 
-public class UserAgentInterceptor implements ClientHttpRequestInterceptor {
+import static dev.fitko.fitconnect.core.http.HttpHeaders.USER_AGENT;
+
+public class UserAgentInterceptor implements Interceptor {
 
     private final String productName;
     private final String productVersion;
@@ -21,13 +22,13 @@ public class UserAgentInterceptor implements ClientHttpRequestInterceptor {
         this.commit = buildInfo.getCommit();
     }
 
+    @NotNull
     @Override
-    public ClientHttpResponse intercept(final HttpRequest req, final byte[] reqBody,
-                                        final ClientHttpRequestExecution ex) throws IOException {
+    public Response intercept(@NotNull Chain chain) throws IOException {
 
-        req.getHeaders().set(HttpHeaders.USER_AGENT, String.format("%s/%s (commit:%s;os:%s)",
-                this.productName, this.productVersion, this.commit, System.getProperty("os.name")));
+        Request request = chain.request().newBuilder().header(USER_AGENT,String.format("%s/%s (commit:%s;os:%s)",
+                this.productName, this.productVersion, this.commit, System.getProperty("os.name"))).build();
 
-        return ex.execute(req, reqBody);
+        return chain.proceed(request);
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
index 2445d98c3..cd1de61d6 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
@@ -15,18 +15,18 @@ import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.keys.KeyService;
 import dev.fitko.fitconnect.api.services.submission.SubmissionService;
 import dev.fitko.fitconnect.api.services.validation.ValidationService;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.HttpHeaders;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 public class PublicKeyService implements KeyService {
@@ -38,17 +38,17 @@ public class PublicKeyService implements KeyService {
     private final ApplicationConfig config;
     private final ValidationService validationService;
     private final SubmissionService submissionService;
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
     private final OAuthService authService;
 
     public PublicKeyService(
             final ApplicationConfig config,
-            final RestTemplate restTemplate,
+            final HttpClient httpClient,
             final OAuthService authService,
             final SubmissionService submissionService,
             final ValidationService validationService) {
         this.config = config;
-        this.restTemplate = restTemplate;
+        this.httpClient = httpClient;
         this.authService = authService;
         this.submissionService = submissionService;
         this.validationService = validationService;
@@ -90,10 +90,11 @@ public class PublicKeyService implements KeyService {
         validateSignatureKey(signatureKey);
         return signatureKey;
     }
+
     @Override
     public RSAKey getWellKnownKeysForSubmissionUrl(final String url, final String keyId) {
         final var requestUrl = !url.endsWith("/") ? url + config.getWellKnownKeysPath() : url;
-        final ApiJwkSet wellKnownKeys = performRequest(requestUrl, ApiJwkSet.class,  getHeadersWithoutAuth(), keyId);
+        final ApiJwkSet wellKnownKeys = performRequest(requestUrl, ApiJwkSet.class, getHeadersWithoutAuth(), keyId);
         final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys());
         validateSignatureKey(signatureKey);
         return signatureKey;
@@ -135,24 +136,26 @@ public class PublicKeyService implements KeyService {
         }
     }
 
-    private <T> T performRequest(final String url, final Class<T> responseType, final HttpHeaders headers, final Object... params) {
-        final HttpEntity<String> entity = new HttpEntity<>(headers);
+    private <T> T performRequest(final String url, final Class<T> responseType, final Map<String, String> headers, final Object... params) {
         try {
-            return restTemplate.exchange(url, HttpMethod.GET, entity, responseType, params).getBody();
-        } catch (final RestClientException e) {
+            return this.httpClient.get(String.format(url, params), headers, responseType).getBody();
+        } catch (final IOException e) {
             throw new RestApiException("Request failed", e);
         }
     }
 
-    private HttpHeaders getHeadersWithoutAuth() {
-        final var headers = new HttpHeaders();
-        headers.setContentType(MediaType.APPLICATION_JSON);
-        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
-        return headers;
+    private Map<String, String> getHeadersWithoutAuth() {
+
+        return new HashMap<>(Map.of(
+                HttpHeaders.CONTENT_TYPE, MimeTypes.APPLICATION_JSON,
+                HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()));
     }
-    private HttpHeaders getHeaders() {
+
+    private Map<String, String> getHeaders() {
+
         final var headers = getHeadersWithoutAuth();
-        headers.setBearerAuth(authService.getCurrentToken().getAccessToken());
+        getHeadersWithoutAuth().put(HttpHeaders.AUTHORIZATION, "Bearer: " + authService.getCurrentToken().getAccessToken());
+
         return headers;
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java
index fe18cf66f..27d2fc5a8 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java
@@ -5,46 +5,45 @@ import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
 import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
 import dev.fitko.fitconnect.api.exceptions.RestApiException;
 import dev.fitko.fitconnect.api.services.routing.RoutingService;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.HttpHeaders;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
 
 import static java.util.function.Predicate.not;
 
 public class RoutingApiService implements RoutingService {
 
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
     private final ApplicationConfig config;
 
-    public RoutingApiService(final ApplicationConfig config, final RestTemplate restTemplate) {
+    public RoutingApiService(final ApplicationConfig config, final HttpClient httpClient) {
         this.config = config;
-        this.restTemplate = restTemplate;
+        this.httpClient = httpClient;
     }
 
     @Override
     public AreaResult getAreas(final List<String> searchExpressions, final int offset, final int limit) {
 
         final String url = config.getAreaEndpoint();
-        final HttpHeaders headers = getHttpHeaders(MediaType.parseMediaType("application/problem+json"));
-        final HttpEntity<String> entity = new HttpEntity<>(headers);
 
-        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url);
-        searchExpressions.forEach(search -> uriBuilder.queryParam("areaSearchexpression", search));
-        uriBuilder.queryParam("offset", offset);
-        uriBuilder.queryParam("limit", limit);
+        List<String> queryParameters = new ArrayList<>();
+        searchExpressions.forEach(search -> queryParameters.add("areaSearchexpression=" + search));
+        queryParameters.add("offset=" + offset);
+        queryParameters.add("limit=" + limit);
+        String queryparameterString = String.join("&", queryParameters);
 
         try {
-            return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, AreaResult.class).getBody();
-        } catch (final RestClientException e) {
+            return this.httpClient.get(url + "?" + queryparameterString, getHttpHeaders(MimeTypes.APPLICATION_PROBLEM_JSON), AreaResult.class).getBody();
+        } catch (final IOException e) {
             throw new RestApiException("Area query failed", e);
         }
     }
@@ -55,28 +54,24 @@ public class RoutingApiService implements RoutingService {
         testIfSearchCriterionIsSet(ars, ags, areaId);
 
         final String url = config.getRoutesEndpoint();
-        final HttpHeaders headers = getHttpHeaders(MediaType.APPLICATION_JSON);
-        final HttpEntity<String> entity = new HttpEntity<>(headers);
-
-        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url);
-        uriBuilder.queryParam("leikaKey", leikaKey);
 
+        List<String> queryParameters = new ArrayList<>();
         if (ars != null) {
-            uriBuilder.queryParam("ars", ars);
+            queryParameters.add("ars=" + ars);
         }
         if (ags != null) {
-            uriBuilder.queryParam("ags", ags);
+            queryParameters.add("ags=" + ags);
         }
         if (areaId != null) {
-            uriBuilder.queryParam("areaId", areaId);
+            queryParameters.add("areaId=" + areaId);
         }
-
-        uriBuilder.queryParam("offset", offset);
-        uriBuilder.queryParam("limit", limit);
+        queryParameters.add("offset=" + offset);
+        queryParameters.add("limit=" + limit);
+        String queryparameterString = String.join("&", queryParameters);
 
         try {
-            return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, RouteResult.class).getBody();
-        } catch (final RestClientException e) {
+            return this.httpClient.get(url + "?" + queryparameterString, getHttpHeaders(MimeTypes.APPLICATION_JSON), RouteResult.class).getBody();
+        } catch (final IOException e) {
             throw new RestApiException("Route query failed", e);
         }
     }
@@ -84,11 +79,11 @@ public class RoutingApiService implements RoutingService {
     private static void testIfSearchCriterionIsSet(final String ars, final String ags, final String areaId) {
         final List<String> areaSearchCriteria = Arrays.asList(ars, ags, areaId);
 
-        if(areaSearchCriteria.stream().allMatch(CriterionEmpty())){
+        if (areaSearchCriteria.stream().allMatch(CriterionEmpty())) {
             throw new RestApiException("At least one search criterion out of ags, ars or areaId must be set");
         }
 
-        if(areaSearchCriteria.stream().filter(not(CriterionEmpty())).count() > 1){
+        if (areaSearchCriteria.stream().filter(not(CriterionEmpty())).count() > 1) {
             throw new RestApiException("Only one of ars, ags or areaId must be specified.");
         }
     }
@@ -97,10 +92,11 @@ public class RoutingApiService implements RoutingService {
         return criterion -> criterion == null || criterion.isEmpty();
     }
 
-    private HttpHeaders getHttpHeaders(final MediaType mediaType) {
-        final var headers = new HttpHeaders();
-        headers.setAccept(List.of(mediaType));
-        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
-        return headers;
+    private Map<String, String> getHttpHeaders(final String mediaType) {
+
+        return new HashMap<>(Map.of(
+                HttpHeaders.ACCEPT, mediaType,
+                HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()
+        ));
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
index d88766126..e2221dd75 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
@@ -7,9 +7,9 @@ import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.InitializationException;
 import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
+import dev.fitko.fitconnect.core.http.HttpClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.web.client.RestTemplate;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -39,16 +39,16 @@ public class SchemaResourceProvider implements SchemaProvider {
 
     private final Map<URI, String> submissionDataSchemas;
 
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
-    public SchemaResourceProvider(final RestTemplate restTemplate, final SchemaResources schemaResources) {
+    public SchemaResourceProvider(final HttpClient httpClient, final SchemaResources schemaResources) {
         setSchemas = new HashMap<>();
         metadataSchemas = new HashMap<>();
         destinationSchemas = new HashMap<>();
         submissionDataSchemas = new HashMap<>();
-        this.restTemplate = restTemplate;
+        this.httpClient = httpClient;
 
         populateSetSchemas(schemaResources.getSetSchemaPaths());
         populateMetadataSchemas(schemaResources.getMetadataSchemaPaths());
@@ -120,7 +120,7 @@ public class SchemaResourceProvider implements SchemaProvider {
 
         if (schemaUri.toString().matches("http.+")) {
             try {
-                return restTemplate.getForEntity(schemaUri, String.class).getBody();
+                return this.httpClient.get(schemaUri.toString(), String.class).getBody();
             } catch (Exception exception) {
                 LOGGER.warn("Fetching schema from " + schemaUri + " failed, falling back to pre-stored version.");
             }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
index bbd10c6c9..debe3e74d 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
@@ -1,9 +1,6 @@
 package dev.fitko.fitconnect.core.submission;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.config.ApplicationConfig;
-import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
 import dev.fitko.fitconnect.api.domain.model.destination.Destination;
 import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission;
 import dev.fitko.fitconnect.api.domain.model.submission.Submission;
@@ -13,214 +10,132 @@ import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
 import dev.fitko.fitconnect.api.exceptions.RestApiException;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.submission.SubmissionService;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.Setter;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.HttpHeaders;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
-
 public class SubmissionApiService implements SubmissionService {
 
     private final OAuthService authService;
-    private final RestTemplate restTemplate;
+    private final HttpClient httpClient;
     private final ApplicationConfig config;
 
-    public SubmissionApiService(final OAuthService authService, final RestTemplate restTemplate, final ApplicationConfig config) {
+    public SubmissionApiService(final OAuthService authService, final HttpClient httpClient, final ApplicationConfig config) {
         this.authService = authService;
-        this.restTemplate = restTemplate;
+        this.httpClient = httpClient;
         this.config = config;
     }
 
     @Override
     public Destination getDestination(final UUID destinationID) throws RestApiException {
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getDestinationsEndpoint())
-                .method(HttpMethod.GET)
-                .responseType(Destination.class)
-                .entity(getHttpEntity(getHeaders()))
-                .params(Map.of("destinationId", destinationID))
-                .errorMessage("Could not get destination " + destinationID)
-                .build();
-        return performRequest(requestSettings);
+
+        final String url = String.format(config.getDestinationsEndpoint(), destinationID);
+
+        try {
+            return this.httpClient.get(url, getHeaders(), Destination.class).getBody();
+
+        } catch (IOException e) {
+            throw new RestApiException("Could not get destination " + destinationID, e);
+        }
     }
 
     @Override
     public SubmissionForPickup announceSubmission(final CreateSubmission submission) throws RestApiException {
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getSubmissionsEndpoint())
-                .method(HttpMethod.POST)
-                .responseType(SubmissionForPickup.class)
-                .entity(getHttpEntity(submission, getHeaders()))
-                .params(Collections.emptyMap())
-                .errorMessage("Could not announce submission for destination " + submission.getDestinationId())
-                .build();
-        return performRequest(requestSettings);
+
+        try {
+            return this.httpClient.post(config.getSubmissionsEndpoint(), getHeaders(), submission, SubmissionForPickup.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Could not announce submission for destination " + submission.getDestinationId(), e);
+        }
     }
 
     @Override
     public void uploadAttachment(final UUID submissionId, final UUID attachmentId, final String encryptedAttachment) throws RestApiException {
-        final Map<String, Object> params = new HashMap<>();
-        params.put("submissionId", submissionId);
-        params.put("attachmentId", attachmentId);
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getAttachmentEndpoint())
-                .method(HttpMethod.PUT)
-                .responseType(Void.class)
-                .entity(getHttpEntity(encryptedAttachment, getHeaders("application/jose")))
-                .params(params)
-                .errorMessage("Could not upload attachment")
-                .build();
-        performRequest(requestSettings);
+
+        final String url = String.format(config.getAttachmentEndpoint(), submissionId, attachmentId);
+
+        try {
+            this.httpClient.put(url, getHeaders("application/jose"), encryptedAttachment, Void.class);
+        } catch (IOException e) {
+            throw new RestApiException("Could not upload attachment", e);
+        }
     }
 
     @Override
     public String getAttachment(final UUID submissionId, final UUID attachmentId) throws RestApiException {
-        final Map<String, Object> params = new HashMap<>();
-        params.put("submissionId", submissionId);
-        params.put("attachmentId", attachmentId);
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getAttachmentEndpoint())
-                .method(HttpMethod.GET)
-                .responseType(String.class)
-                .entity(getHttpEntity(getHeaders("application/jose")))
-                .params(params)
-                .errorMessage("Could not get attachment " + attachmentId)
-                .build();
-        return performRequest(requestSettings);
+
+        final String url = String.format(config.getAttachmentEndpoint(), submissionId, attachmentId);
+
+        try {
+            return this.httpClient.get(url, getHeaders("application/jose"), String.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Could not get attachment " + attachmentId, e);
+        }
     }
 
     @Override
     public Submission sendSubmission(final SubmitSubmission submission) throws RestApiException {
-        final Map<String, Object> params = Map.of("submissionId", submission.getSubmissionId());
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getSubmissionEndpoint())
-                .method(HttpMethod.PUT)
-                .responseType(Submission.class)
-                .entity(getHttpEntity(submission, getHeaders()))
-                .params(params)
-                .errorMessage("Could not send submission " + submission.getSubmissionId())
-                .build();
-        return performRequest(requestSettings);
+
+        final String url = String.format(config.getSubmissionEndpoint(), submission);
+
+        try {
+            return this.httpClient.put(url, getHeaders(), submission, Submission.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Could not send submission " + submission.getSubmissionId(), e);
+        }
     }
 
     @Override
     public SubmissionsForPickup pollAvailableSubmissions(final int offset, final int limit) throws RestApiException {
-        final String urlWithQueryParams = UriComponentsBuilder.fromHttpUrl(config.getSubmissionsEndpoint())
-                .queryParam("limit", limit)
-                .queryParam("offset", offset)
-                .encode()
-                .toUriString();
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(urlWithQueryParams)
-                .method(HttpMethod.GET)
-                .responseType(SubmissionsForPickup.class)
-                .entity(getHttpEntity(getHeaders()))
-                .errorMessage("Could not poll for available submissions")
-                .build();
-        return performRequest(requestSettings);
+
+        final String urlWithQueryParams = config.getSubmissionsEndpoint() + "?limit=" + limit + "&offset=" + offset;
+
+        try {
+            return this.httpClient.get(urlWithQueryParams, getHeaders(), SubmissionsForPickup.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Could not poll for available submissions", e);
+        }
     }
 
     @Override
     public SubmissionsForPickup pollAvailableSubmissionsForDestination(final UUID destinationId, final int offset, final int limit) {
-        final String urlWithQueryParams = UriComponentsBuilder.fromHttpUrl(config.getSubmissionsEndpoint())
-                .queryParam("destinationId", destinationId)
-                .queryParam("limit", limit)
-                .queryParam("offset", offset)
-                .encode()
-                .toUriString();
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(urlWithQueryParams)
-                .method(HttpMethod.GET)
-                .responseType(SubmissionsForPickup.class)
-                .entity(getHttpEntity(getHeaders()))
-                .errorMessage("Could not poll for available submissions on destination " + destinationId)
-                .build();
-        return performRequest(requestSettings);
-    }
 
-    @Override
-    public Submission getSubmission(final UUID submissionId) throws RestApiException {
-        final Map<String, Object> params = Map.of("submissionId", submissionId);
-        final RequestSettings requestSettings = RequestSettings.builder()
-                .url(config.getSubmissionEndpoint())
-                .method(HttpMethod.GET)
-                .responseType(Submission.class)
-                .entity(getHttpEntity(getHeaders()))
-                .params(params)
-                .errorMessage("Submission with id " + submissionId + " does not exist")
-                .build();
-        return performRequest(requestSettings);
-    }
+        final String urlWithQueryParams = config.getSubmissionsEndpoint() + "?destinationId=" + destinationId
+                + "&limit=" + limit + "&offset=" + offset;
 
-    private <T> T performRequest(final RequestSettings requestSettings) throws RestApiException {
-        final String url = requestSettings.url;
-        final HttpMethod method = requestSettings.method;
-        final HttpEntity entity = requestSettings.entity;
-        final Class<T> responseType = requestSettings.responseType;
-        final Map<String, ?> params = requestSettings.params;
         try {
-            return restTemplate.exchange(url, method, entity, responseType, params).getBody();
-        } catch (final RestClientException e) {
-            throw new RestApiException(e.getMessage(), e);
+            return this.httpClient.get(urlWithQueryParams, getHeaders(), SubmissionsForPickup.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Could not poll for available submissions on destination " + destinationId, e);
         }
     }
 
-    private HttpEntity<String> getHttpEntity(final HttpHeaders headers) {
-        return getHttpEntity(null, headers);
-    }
+    @Override
+    public Submission getSubmission(final UUID submissionId) throws RestApiException {
 
-    private HttpEntity<String> getHttpEntity(final Object body, final HttpHeaders headers) {
-        if (body == null) {
-            return new HttpEntity<>(headers);
-        }
         try {
-            return getHttpEntity(new ObjectMapper().writeValueAsString(body), headers);
-        } catch (final JsonProcessingException e) {
-            throw new RestApiException("Request body could not be processed", e);
+            return this.httpClient.get(String.format(config.getSubmissionEndpoint(), submissionId), getHeaders(), Submission.class).getBody();
+        } catch (IOException e) {
+            throw new RestApiException("Submission with id " + submissionId + " does not exist", e);
         }
     }
 
-    private HttpEntity<String> getHttpEntity(final String body, final HttpHeaders headers) {
-        return body == null ? new HttpEntity<>(headers) : new HttpEntity<>(body, headers);
-    }
-
-    private HttpHeaders getHeaders() {
-        return getHeaders(APPLICATION_JSON_VALUE);
+    private Map<String, String> getHeaders() {
+        return getHeaders(MimeTypes.APPLICATION_JSON);
     }
 
-    private HttpHeaders getHeaders(final String contentType) {
-        final OAuthToken token = authService.getCurrentToken();
-        final var headers = new HttpHeaders();
-        headers.setBearerAuth(token.getAccessToken());
-        headers.setContentType(MediaType.parseMediaType(contentType));
-        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
-        return headers;
-    }
+    private Map<String, String> getHeaders(final String contentType) {
 
-    @Getter
-    @Setter
-    @Builder
-    private static class RequestSettings {
-        private String url;
-        private HttpMethod method;
-        private HttpEntity entity;
-        private Class responseType;
-        private String errorMessage;
-        @Builder.Default
-        private Map<String, Object> params = new HashMap<>();
+        return new HashMap<>(Map.of(
+                HttpHeaders.AUTHORIZATION, "Bearer: " + authService.getCurrentToken(),
+                HttpHeaders.CONTENT_TYPE, contentType,
+                HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString()
+        ));
     }
 }
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/auth/DefaultOAuthServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/auth/DefaultOAuthServiceTest.java
index 85189df78..dbddea4d7 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/auth/DefaultOAuthServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/auth/DefaultOAuthServiceTest.java
@@ -5,10 +5,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.core.RestEndpointBase;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.ok;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
@@ -24,7 +24,7 @@ class DefaultOAuthServiceTest extends RestEndpointBase {
     public void setUp() {
         wireMockServer.resetMappings();
         final var config = getTestConfig("http://localhost:" +  wireMockServer.port());
-        underTest = new DefaultOAuthService(new RestTemplate(), "id", "secret", config.getOAuthTokenEndpoint());
+        underTest = new DefaultOAuthService(new HttpClient(true), "id", "secret", config.getOAuthTokenEndpoint());
     }
 
     @Test
@@ -74,7 +74,7 @@ class DefaultOAuthServiceTest extends RestEndpointBase {
                 post(urlEqualTo("/token"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(token))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
     }
 }
\ No newline at end of file
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogApiServiceTest.java
index 787962c59..000635d95 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogApiServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogApiServiceTest.java
@@ -6,8 +6,8 @@ import com.github.tomakehurst.wiremock.http.ResponseDefinition;
 import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
 import dev.fitko.fitconnect.api.domain.model.event.EventLog;
 import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
-import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus;
 import dev.fitko.fitconnect.api.domain.model.event.SubmissionState;
+import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus;
 import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
 import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
 import dev.fitko.fitconnect.api.exceptions.EventLogException;
@@ -15,11 +15,11 @@ import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.events.EventLogService;
 import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
 import dev.fitko.fitconnect.core.RestEndpointBase;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentMatchers;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
 
 import java.util.Collections;
 import java.util.List;
@@ -57,7 +57,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
         authServiceMock = mock(OAuthService.class);
         verifierMock = mock(EventLogVerificationService.class);
         final var config = getTestConfig("http://localhost:" + wireMockServer.port());
-        underTest = new EventLogApiService(config, authServiceMock, new RestTemplate(), verifierMock);
+        underTest = new EventLogApiService(config, authServiceMock, new HttpClient(true), verifierMock);
     }
 
     @Test
@@ -83,7 +83,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedLog))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
@@ -119,7 +119,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedLog))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
@@ -153,7 +153,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedLog))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
@@ -187,7 +187,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedLog))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
@@ -215,7 +215,7 @@ class EventLogApiServiceTest extends RestEndpointBase {
                 post(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(serialisedSetEvent)
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(204))).getResponse();
 
         // When
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
index 4cd75dd84..c90c7c7e3 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
@@ -23,12 +23,12 @@ import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
 import dev.fitko.fitconnect.api.services.validation.ValidationService;
 import dev.fitko.fitconnect.core.crypto.HashService;
 import dev.fitko.fitconnect.core.crypto.JWECryptoService;
+import dev.fitko.fitconnect.core.http.HttpClient;
 import dev.fitko.fitconnect.core.schema.SchemaResourceProvider;
 import dev.fitko.fitconnect.core.util.CertificateLoader;
 import dev.fitko.fitconnect.core.validation.DefaultValidationService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.web.client.RestTemplate;
 
 import java.io.File;
 import java.io.IOException;
@@ -68,7 +68,7 @@ class SecurityEventTokenServiceTest {
         final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
         final String submissionDataSchemaPath = "submission-data-test-schema";
         final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas, submissionDataSchemaPath);
-        final SchemaProvider schemaProvider = new SchemaResourceProvider(mock(RestTemplate.class), schemaResources);
+        final SchemaProvider schemaProvider = new SchemaResourceProvider(mock(HttpClient.class), schemaResources);
 
         validationService = new DefaultValidationService(config, new HashService(), schemaProvider,
                 CertificateLoader.create().loadTrustedRootCertificates("trusted-test-root-certificates"));
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
index 2a832c8e5..9c85e7460 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
@@ -6,8 +6,8 @@ import dev.fitko.fitconnect.api.config.BuildInfo;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.HttpMethod;
-import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
 import static com.github.tomakehurst.wiremock.client.WireMock.get;
@@ -32,7 +32,7 @@ public class ProxyRestTemplateTest {
     }
 
     @Test
-    void userAgentIsCorrect() {
+    void userAgentIsCorrect() throws IOException {
 
         WireMock.configureFor("localhost", 8080);
         wireMockServer.stubFor(get(urlEqualTo("/test"))
@@ -44,9 +44,9 @@ public class ProxyRestTemplateTest {
         buildInfo.setProductName("productName");
         buildInfo.setProductVersion("productVersion");
         buildInfo.setCommit("commit");
-        final RestTemplate restTemplate = new RestService(null, 0, buildInfo).getRestTemplate();
+        final HttpClient httpClient = new RestService(null, 0, buildInfo).getHttpClient();
 
-        restTemplate.exchange("http://localhost:8080/test", HttpMethod.GET, null, String.class);
+        httpClient.get("http://localhost:8080/test", String.class);
 
         verify(getRequestedFor(urlPathEqualTo("/test"))
                 .withHeader("User-Agent", equalTo("productName/productVersion (commit:commit;os:" + System.getProperty("os.name") + ")")));
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
index 3b3830ec5..f3d61ae70 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
@@ -2,11 +2,7 @@ package dev.fitko.fitconnect.core.http;
 
 import dev.fitko.fitconnect.api.config.BuildInfo;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.client.InterceptingClientHttpRequestFactory;
-import org.springframework.web.client.RestTemplate;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -23,12 +19,10 @@ class RestServiceTest {
         underTest = new RestService("http://testhost.de", 8080, new BuildInfo());
 
         // When
-        final RestTemplate template = underTest.getRestTemplate();
+        final HttpClient httpClient = underTest.getHttpClient();
 
         // Then
-        assertNotNull(template);
-        assertThat(template.getRequestFactory().getClass(), equalTo(InterceptingClientHttpRequestFactory.class));
-
+        assertNotNull(httpClient);
     }
 
     @Test
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
index 7a9c0bc41..3a4f59b3e 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
@@ -1,6 +1,5 @@
 package dev.fitko.fitconnect.core.http;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.config.BuildInfo;
 import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
@@ -9,36 +8,28 @@ import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
 import dev.fitko.fitconnect.core.RestEndpointBase;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.net.URI;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.get;
 import static com.github.tomakehurst.wiremock.client.WireMock.ok;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.greaterThan;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 public class RestTemplateTest extends RestEndpointBase {
 
-    private RestTemplate underTest;
+    private HttpClient underTest;
 
     @BeforeEach
     void setup() {
-        underTest = new RestService(null, 0, new BuildInfo()).getRestTemplate();
+        underTest = new RestService(null, 0, new BuildInfo()).getHttpClient();
     }
 
     @Test
-    void testRestCallForUnknownProperties() throws JsonProcessingException {
+    void testRestCallForUnknownProperties() throws IOException {
 
         // Given
         final var fakeBaseUrl = "http://localhost:" + wireMockServer.port();
@@ -49,39 +40,16 @@ public class RestTemplateTest extends RestEndpointBase {
                 get(urlEqualTo("/test"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(metadataWithUnknownProperty))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
         
         // When
-        final Metadata response = underTest.getForObject(URI.create(fakeBaseUrl + "/test"), Metadata.class);
+        final Metadata response = underTest.get(URI.create(fakeBaseUrl + "/test").toString(), Metadata.class).getBody();
 
         // Then
         assertNotNull(response);
     }
 
-    @Test
-    void testMessageConversionForUnknownProperties() throws JsonProcessingException {
-
-        // Given
-        final List<HttpMessageConverter<?>> messageConverters = underTest.getMessageConverters();
-        assertThat(messageConverters.size(), is(greaterThan(0)));
-
-        // Get last set converter that's expected to be the jackson object mapper
-        final HttpMessageConverter<?> httpMessageConverter = messageConverters.get(messageConverters.size()-1);
-        assertThat(httpMessageConverter, instanceOf(MappingJackson2HttpMessageConverter.class));
-
-        final MappingJackson2HttpMessageConverter converter = (MappingJackson2HttpMessageConverter) httpMessageConverter;
-
-        final ObjectMapper mapper = converter.getObjectMapper();
-
-        final Map<String, Map<String, Object>> metadataWithUnknownProperty = getMetadataWithUnknownSchemaProperty();
-
-        // Map object with an unknown property
-        final Metadata metadata = mapper.readValue(mapper.writeValueAsString(metadataWithUnknownProperty), Metadata.class);
-
-        assertNotNull(metadata);
-    }
-
     private static Map<String, Map<String, Object>> getMetadataWithUnknownSchemaProperty() {
         return Map.of(
                 "$schemaFoo", Collections.emptyMap(),
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
index 82e637c36..cc8bd1018 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
@@ -16,10 +16,10 @@ import dev.fitko.fitconnect.api.services.keys.KeyService;
 import dev.fitko.fitconnect.api.services.submission.SubmissionService;
 import dev.fitko.fitconnect.api.services.validation.ValidationService;
 import dev.fitko.fitconnect.core.RestEndpointBase;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
 
 import java.io.IOException;
 import java.text.ParseException;
@@ -65,7 +65,7 @@ class PublicKeyServiceTest extends RestEndpointBase {
         config.setEnvironments(Map.of(envName, environment));
         config.setActiveEnvironment(envName);
 
-        underTest = new PublicKeyService(config, new RestTemplate(), authServiceMock, submissionServiceMock, validationServiceMock);
+        underTest = new PublicKeyService(config, new HttpClient(true), authServiceMock, submissionServiceMock, validationServiceMock);
     }
 
     @Test
@@ -86,7 +86,7 @@ class PublicKeyServiceTest extends RestEndpointBase {
                         .withHeader("Authorization", containing("Bearer " + authToken.getAccessToken()))
                         .willReturn(ok()
                                 .withBody(publicKeyJson)
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
@@ -122,14 +122,14 @@ class PublicKeyServiceTest extends RestEndpointBase {
         config.setEnvironments(Map.of(envName, environment));
         config.setActiveEnvironment(envName);
 
-        final PublicKeyService keyService = new PublicKeyService(config, new RestTemplate(), authServiceMock, submissionServiceMock, validationServiceMock);
+        final PublicKeyService keyService = new PublicKeyService(config, new HttpClient(true), authServiceMock, submissionServiceMock, validationServiceMock);
 
         wireMockServer.stubFor(
                 get(urlEqualTo("/v1/destinations/" + destination.getDestinationId() + "/keys/123"))
                         .withHeader("Authorization", containing("Bearer " + authToken.getAccessToken()))
                         .willReturn(ok()
                                 .withBody(publicKeyJson)
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
@@ -160,7 +160,7 @@ class PublicKeyServiceTest extends RestEndpointBase {
                 get(urlEqualTo("/v1/destinations/" + destination.getDestinationId() + "/keys/123"))
                         .willReturn(ok()
                                 .withBody(expectedSignatureKey)
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
@@ -185,13 +185,13 @@ class PublicKeyServiceTest extends RestEndpointBase {
         final OAuthToken authToken = new OAuthToken();
         authToken.setAccessToken("abc123");
 
-        final JWKSet jwkSet = new JWKSet(JWK.parse( getResourceAsString("/public_signature_test_key.json")));
+        final JWKSet jwkSet = new JWKSet(JWK.parse(getResourceAsString("/public_signature_test_key.json")));
 
         wireMockServer.stubFor(
                 get(urlEqualTo("/.well-known/jwks.json"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(jwkSet.toJSONObject()))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
@@ -215,13 +215,13 @@ class PublicKeyServiceTest extends RestEndpointBase {
         final OAuthToken authToken = new OAuthToken();
         authToken.setAccessToken("abc123");
 
-        final JWKSet jwkSet = new JWKSet(JWK.parse( getResourceAsString("/public_signature_test_key.json")));
+        final JWKSet jwkSet = new JWKSet(JWK.parse(getResourceAsString("/public_signature_test_key.json")));
 
         wireMockServer.stubFor(
                 get(urlEqualTo("/.well-known/jwks.json"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(jwkSet.toJSONObject()))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
@@ -251,7 +251,7 @@ class PublicKeyServiceTest extends RestEndpointBase {
                 get(urlEqualTo("/custom/path/.well-known/jwks.json"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(jwkSet.toJSONObject()))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         when(authServiceMock.getCurrentToken()).thenReturn(authToken);
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java
index 2c291cc79..2a7b4d20a 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java
@@ -12,10 +12,10 @@ import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
 import dev.fitko.fitconnect.api.exceptions.RestApiException;
 import dev.fitko.fitconnect.api.services.routing.RoutingService;
 import dev.fitko.fitconnect.core.RestEndpointBase;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
 
 import java.util.List;
 import java.util.Map;
@@ -48,7 +48,7 @@ public class RoutingApiServiceTest extends RestEndpointBase {
         config.setEnvironments(Map.of(envName, environment));
         config.setActiveEnvironment(envName);
 
-        underTest = new RoutingApiService(config, new RestTemplate());
+        underTest = new RoutingApiService(config, new HttpClient(true));
     }
 
     @Test
@@ -99,7 +99,7 @@ public class RoutingApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo("/v1/routes?leikaKey=99123456760610&ars=064350014014&offset=0&limit=10"))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedResult))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
index 17033c781..42a944043 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
@@ -6,18 +6,21 @@ import dev.fitko.fitconnect.api.config.SchemaConfig;
 import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.HttpResponse;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
 import java.net.URI;
 import java.util.List;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -34,7 +37,7 @@ class SchemaResourceProviderTest {
         final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
         final String submissionDataPath = "submission-data-test-schema";
         final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas, submissionDataPath);
-        underTest = new SchemaResourceProvider(mock(RestTemplate.class), schemaResources);
+        underTest = new SchemaResourceProvider(mock(HttpClient.class), schemaResources);
     }
 
     @Test
@@ -102,12 +105,12 @@ class SchemaResourceProviderTest {
     }
 
     @Test
-    void loadSubmissionDataSchemaFromRemote() {
+    void loadSubmissionDataSchemaFromRemote() throws IOException {
 
-        RestTemplate restTemplate = mock(RestTemplate.class);
-        when(restTemplate.getForEntity(eq(URI.create("https://test.json")), any())).thenReturn(ResponseEntity.ok("schema"));
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.get(eq("https://test.json"), any())).thenReturn(new HttpResponse<>(200, "schema"));
 
-        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(restTemplate,
+        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(httpClient,
                 new SchemaResources(List.of(), List.of(), List.of(), null));
 
         String schema = schemaResourceProvider.loadSubmissionDataSchema(URI.create("https://test.json"));
@@ -116,13 +119,13 @@ class SchemaResourceProviderTest {
     }
 
     @Test
-    void loadSubmissionDataSchemaFromRemoteFailedButFromMemoryWorks() {
+    void loadSubmissionDataSchemaFromRemoteFailedButFromMemoryWorks() throws IOException {
 
-        RestTemplate restTemplate = mock(RestTemplate.class);
-        when(restTemplate.getForEntity(eq(URI.create("https://schema.fitko.de/fim/s00000096_1.0.schema.json")),
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.get(eq("https://schema.fitko.de/fim/s00000096_1.0.schema.json"),
                 any())).thenThrow(new RuntimeException("Something went wrong!"));
 
-        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(restTemplate,
+        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(httpClient,
                 new SchemaResources(List.of(), List.of(), List.of(), "submission-data-test-schema"));
 
         String schema = schemaResourceProvider.loadSubmissionDataSchema(URI.create("https://schema.fitko.de/fim/s00000096_1.0.schema.json"));
@@ -131,13 +134,13 @@ class SchemaResourceProviderTest {
     }
 
     @Test
-    void loadSubmissionDataSchemaFromRemoteAndMemoryFailed() {
+    void loadSubmissionDataSchemaFromRemoteAndMemoryFailed() throws IOException {
 
-        RestTemplate restTemplate = mock(RestTemplate.class);
-        when(restTemplate.getForEntity(eq(URI.create("https://schema.fitko.de/fim/s00000096_1.0.schema.json")),
+        HttpClient httpClient = mock(HttpClient.class);
+        when(httpClient.get(eq("https://schema.fitko.de/fim/s00000096_1.0.schema.json"),
                 any())).thenThrow(new RuntimeException("Something went wrong!"));
 
-        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(restTemplate,
+        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(httpClient,
                 new SchemaResources(List.of(), List.of(), List.of(), null));
 
         Exception exception = assertThrows(SchemaNotFoundException.class,
@@ -149,7 +152,7 @@ class SchemaResourceProviderTest {
     @Test
     void loadSubmissionDataSchemaFromMemory() {
 
-        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(mock(RestTemplate.class),
+        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(mock(HttpClient.class),
                 new SchemaResources(List.of(), List.of(), List.of(), "submission-data-test-schema"));
 
         String schema = schemaResourceProvider.loadSubmissionDataSchema(URI.create("urn:test:submission_data_schema.json"));
@@ -160,7 +163,7 @@ class SchemaResourceProviderTest {
     @Test
     void loadSubmissionDataSchemaFromMemoryFailed() {
 
-        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(mock(RestTemplate.class),
+        SchemaResourceProvider schemaResourceProvider = new SchemaResourceProvider(mock(HttpClient.class),
                 new SchemaResources(List.of(), List.of(), List.of(), null));
 
         Exception exception = assertThrows(SchemaNotFoundException.class,
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
index e257c31c9..b8aa4ce5b 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
@@ -15,12 +15,11 @@ import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.submission.SubmissionService;
 import dev.fitko.fitconnect.core.RestEndpointBase;
+import dev.fitko.fitconnect.core.http.HttpClient;
+import dev.fitko.fitconnect.core.http.MimeTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.web.client.RestTemplate;
 
 import java.util.HashSet;
 import java.util.List;
@@ -46,7 +45,7 @@ import static org.mockito.Mockito.when;
 
 class SubmissionApiServiceTest extends RestEndpointBase {
 
-    private RestTemplate restTemplate;
+    private HttpClient httpClient;
     private OAuthService authServiceMock;
     private SubmissionService underTest;
 
@@ -56,9 +55,9 @@ class SubmissionApiServiceTest extends RestEndpointBase {
         final var fakeBaseUrl = "http://localhost:" + wireMockServer.port();
         final ApplicationConfig config = getTestConfig(fakeBaseUrl);
 
-        restTemplate = new RestTemplate();
+        httpClient = new HttpClient(true);
         authServiceMock = Mockito.mock(OAuthService.class);
-        underTest = new SubmissionApiService(authServiceMock, restTemplate, config);
+        underTest = new SubmissionApiService(authServiceMock, httpClient, config);
     }
 
     @Test
@@ -95,7 +94,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                         .withHeader("Authorization", containing("Bearer " + authToken.getAccessToken()))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedSubmission))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200)));
 
         // When
@@ -135,7 +134,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
         // Then
         verify(putRequestedFor(urlEqualTo(urlPath)).withHeader("Authorization", equalTo("Bearer abc")));
         assertThat(response.getHeaders().getContentTypeHeader().mimeTypePart(), is("application/jose"));
-        assertEquals(response.getStatus(), HttpStatus.NO_CONTENT.value());
+        assertEquals(response.getStatus(), MimeTypes.APPLICATION_JSON);
     }
 
     @Test
@@ -168,7 +167,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
         assertThat(attachment, is(encryptedAttachment));
         verify(getRequestedFor(urlEqualTo(urlPath)).withHeader("Authorization", equalTo("Bearer abc")));
         assertThat(response.getHeaders().getContentTypeHeader().mimeTypePart(), is("application/jose"));
-        assertEquals(response.getStatus(), HttpStatus.OK.value());
+        assertEquals(response.getStatus(), MimeTypes.APPLICATION_JSON);
     }
 
     @Test
@@ -200,10 +199,9 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                 put(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedSubmission))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200))).getResponse();
 
-
         // When
         final Submission submission = underTest.sendSubmission(submitSubmission);
 
@@ -215,7 +213,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
 
         verify(putRequestedFor(urlEqualTo(urlPath)).withHeader("Authorization", equalTo("Bearer abc")));
         assertThat(response.getHeaders().getContentTypeHeader().mimeTypePart(), is("application/json"));
-        assertEquals(response.getStatus(), HttpStatus.OK.value());
+        assertEquals(response.getStatus(), 200);
     }
 
     @Test
@@ -249,7 +247,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedSubmissions))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200))).getResponse();
 
         // When
@@ -281,7 +279,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedSubmission))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200))).getResponse();
 
         // When
@@ -309,7 +307,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                 get(urlEqualTo(urlPath))
                         .willReturn(ok()
                                 .withBody(new ObjectMapper().writeValueAsString(expectedDestination))
-                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withHeader("Content-Type", MimeTypes.APPLICATION_JSON)
                                 .withStatus(200))).getResponse();
         // When
         final Destination destination = underTest.getDestination(expectedDestination.getDestinationId());
@@ -319,7 +317,7 @@ class SubmissionApiServiceTest extends RestEndpointBase {
         verify(getRequestedFor(urlEqualTo(urlPath)).withHeader("Authorization", equalTo("Bearer 123")));
     }
 
-    private Set<SubmissionForPickup> generateSubmission(final UUID destinationId, final int count){
+    private Set<SubmissionForPickup> generateSubmission(final UUID destinationId, final int count) {
         final Set<SubmissionForPickup> submissions = new HashSet<>(count);
         for (int i = 0; i < count; i++) {
             final SubmissionForPickup submission = new SubmissionForPickup();
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
index 42761009b..a25ada95b 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
@@ -50,6 +50,7 @@ import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
 import dev.fitko.fitconnect.api.services.crypto.MessageDigestService;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
 import dev.fitko.fitconnect.core.crypto.HashService;
+import dev.fitko.fitconnect.core.http.HttpClient;
 import dev.fitko.fitconnect.core.schema.SchemaResourceProvider;
 import dev.fitko.fitconnect.core.testutil.LogCaptor;
 import dev.fitko.fitconnect.core.util.CertificateLoader;
@@ -57,7 +58,6 @@ import dev.fitko.fitconnect.jwkvalidator.exceptions.JWKValidationException;
 import org.hamcrest.CoreMatchers;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.web.client.RestTemplate;
 
 import java.io.IOException;
 import java.net.URI;
@@ -92,7 +92,7 @@ class DefaultValidationServiceTest {
     private DefaultValidationService underTest;
     private MessageDigestService hashService;
     private static final LogCaptor logs = new LogCaptor();
-    private final RestTemplate restTemplate = mock(RestTemplate.class);
+    private final HttpClient httpClient = mock(HttpClient.class);
     private SchemaProvider schemaProvider;
 
     @BeforeEach
@@ -104,7 +104,7 @@ class DefaultValidationServiceTest {
         final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
         final String submissionDataSchemaPath = "submission-data-test-schema";
         final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas, submissionDataSchemaPath);
-        schemaProvider = new SchemaResourceProvider(restTemplate, schemaResources);
+        schemaProvider = new SchemaResourceProvider(httpClient, schemaResources);
         underTest = new DefaultValidationService(config, hashService, schemaProvider,
                 CertificateLoader.create().loadTrustedRootCertificates("trusted-test-root-certificates"));
     }
diff --git a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/AuthenticationIT.java b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/AuthenticationIT.java
index 0da0e02ba..27432d7d1 100644
--- a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/AuthenticationIT.java
+++ b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/AuthenticationIT.java
@@ -25,7 +25,7 @@ public class AuthenticationIT {
 
         final RestService restService = new RestService(new BuildInfo());
 
-        final var authService = new DefaultOAuthService(restService.getRestTemplate(), clientId, secret, tokenUrl);
+        final var authService = new DefaultOAuthService(restService.getHttpClient(), clientId, secret, tokenUrl);
 
         // When
         final OAuthToken token = authService.getCurrentToken();
diff --git a/pom.xml b/pom.xml
index 14e3ed226..1b55ce66c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,7 +68,6 @@
         <slf4j.version>2.0.7</slf4j.version>
         <jcommander.version>1.82</jcommander.version>
         <apache-tika.version>2.7.0</apache-tika.version>
-        <spring-web.version>5.3.26</spring-web.version>
         <snakeyaml.version>2.0</snakeyaml.version>
         <open-csv.version>5.7.1</open-csv.version>
         <json-schema-validator.version>1.0.79</json-schema-validator.version>
@@ -159,11 +158,6 @@
                 <artifactId>nimbus-jose-jwt</artifactId>
                 <version>${nimbus.version}</version>
             </dependency>
-            <dependency>
-                <groupId>org.springframework</groupId>
-                <artifactId>spring-web</artifactId>
-                <version>${spring-web.version}</version>
-            </dependency>
             <dependency>
                 <groupId>ch.qos.logback</groupId>
                 <artifactId>logback-classic</artifactId>
-- 
GitLab