{"id":20983263,"url":"https://github.com/novatecconsulting/showcase-spring-http","last_synced_at":"2025-12-25T18:05:12.945Z","repository":{"id":76666673,"uuid":"363435968","full_name":"NovatecConsulting/showcase-spring-http","owner":"NovatecConsulting","description":"Demonstrate different approaches for HTTP based communication in Spring","archived":false,"fork":false,"pushed_at":"2021-06-29T13:18:29.000Z","size":259,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-01-20T07:08:16.326Z","etag":null,"topics":["http","pai","showcase","spring"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NovatecConsulting.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-01T14:55:30.000Z","updated_at":"2022-11-17T13:12:12.000Z","dependencies_parsed_at":"2023-02-27T15:31:42.850Z","dependency_job_id":null,"html_url":"https://github.com/NovatecConsulting/showcase-spring-http","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NovatecConsulting%2Fshowcase-spring-http","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NovatecConsulting%2Fshowcase-spring-http/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NovatecConsulting%2Fshowcase-spring-http/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NovatecConsulting%2Fshowcase-spring-http/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NovatecConsulting","download_url":"https://codeload.github.com/NovatecConsulting/showcase-spring-http/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243392281,"owners_count":20283559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","pai","showcase","spring"],"created_at":"2024-11-19T05:48:22.106Z","updated_at":"2025-12-25T18:05:12.849Z","avatar_url":"https://github.com/NovatecConsulting.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":":toc:\n:toc-title:\n:toclevels: 2\n:sectnums:\n\n= HTTP Examples with Spring Boot\n\nThis showcase has the goal to demonstrate different libraries for HTTP communication with Spring Boot:\n\n* Spring Web\n* Spring WebFlux (Reactive)\n\nIn addition, some pitfalls of those libraries are demonstrated and how to solve them.\n\nApart from this, this showcase shows how to monitor important application metrics, like server and client connections as well as thread pool stats.\n\n== Quick Start\n\n.Build Docker Images\n[source,bash]\n----\n./gradlew bootBuildImage\n----\n\n.Run Demo Environment\n[source,bash]\n----\ndocker-compose up\n----\n\nThe demo environment includes all demo applications of this repository, as well as a small\nmonitoring platform which includes Prometheus and Grafana, as well as Consul as Service Registry in order to automatically\nregister new services as targets to Prometheus.\n\nIf you open your browser at http://localhost:3000, Grafana is opened. The Dashboard `HTTP Connections` provides a lot of\ninformation about the internals of this demo applications.\n\n.Stop Demo Environment\n[source,bash]\n----\ndocker-compose down -v\n----\n\n== Scenarios\n\nTo evaluate the behavior of the different implementations the _delegate_ example services all provide\na `GET /delegate/demo` and a `GET /delegate/subcalldemo` endpoint.\n\n`GET /delegate/demo` directly forwards the request to the `GET /demo` endpoint of the corresponding _server_ application.\nIn order to evaluate behaviour of multiple parallel calls, the _server_ endpoint blocks for a specified amount of time,\nwhich is by default 10s. This amount of time can be specified with the query parameter `processDuration`\n(for example `GET /delegate/demo?processDuration=PT10S`).\n\n`GET /delegate/subcalldemo` is similar but executes a configurable number of parallel sub-calls to\n`GET /demo` endpoint of the corresponding _server_ application.\nBy default, 50 sub-calls are executed, each taking 100ms before the _server_ application responses.\nThis means, that if all sub-calls would be executed in sequence, the call would take 5s.\nThe number of sub-calls, as well as the duration per sub-call can be configured with the query parameters\n`subCalls` and `subCallDuration` (for example `GET /delegate/subcalldemo?subCalls=50\u0026subCallDuration=PT0.1S`).\n\nTo give you a detailed overview about the behaviour the used connection pools and executors are bound to\nMicrometer's meter registry.\nIf you navigate to http://localhost:3000/ and open the Dashboard `HTTP Connections`, you find a comprehensive overview about all demo applications.\n\nThis helps you to experiment with the configurations in order to get a better understanding of their consequences.\n\n=== HTTP/1.1 with Spring Web using RestTemplate and Connection Pool\n\nThe Spring Web _delegate_ application configures a _RestTemplate_ with a default configured HTTP connection pool.\n\n[source,java]\n----\n@Configuration\npublic class RestTemplateConfiguration {\n    @Bean\n    public RestTemplate restTemplate(RestTemplateBuilder builder) {\n        return builder\n                .requestFactory(() -\u003e new HttpComponentsClientHttpRequestFactory())\n                .build();\n    }\n}\n----\n\nNow let's execute a call to `GET /delegate/subcalldemo`:\n\n.Query\n[source,bash]\n----\ntime curl http://localhost:8900/delegate/subcalldemo \u0026\n----\n\nThe request will return after ~1 second (50 sub-calls could be executed within 1 second).\n\n.Response\n----\ncurl http://localhost:8900/delegate/subcalldemo  0,00s user 0,00s system 0% cpu 1,032 total\n----\n\nIf you execute this command multiple times one right after the other, you will recognize that the execution time increases.\n\n.Responses\n----\ncurl http://localhost:8900/delegate/subcalldemo  0,00s user 0,00s system 0% cpu 1,027 total\ncurl http://localhost:8900/delegate/subcalldemo  0,00s user 0,00s system 0% cpu 1,823 total\ncurl http://localhost:8900/delegate/subcalldemo  0,00s user 0,00s system 0% cpu 2,369 total\ncurl http://localhost:8900/delegate/subcalldemo  0,01s user 0,00s system 0% cpu 2,918 total\ncurl http://localhost:8900/delegate/subcalldemo  0,00s user 0,01s system 0% cpu 3,606 total\n----\n\nNow let's do the same with the `GET /delegate/demo` endpoint (which blocks for 10s in the _server_ application).\nExecute this command 10 times one right after the other.\n\n.Query\n[source,bash]\n----\nfor i in $(seq 1 10); do time curl http://localhost:8900/delegate/demo \u0026; done\n----\n\nYou will recognize, that 5 calls return after 10 seconds, and another 5 calls after 20 seconds.\n\nThe reason for this is, that by default the used `HttpComponentsClientHttpRequestFactory` creates a connection pool,\nwhich only allows 5 connections per host. That is basically the reason why only 5 calls are executed in parallel\nand why the sub call command takes 1 second. If additional commands are executed, they need to wait for free connections.\n\nThe number of connections can be specified with the following configuration:\n\n[source,java]\n----\n@Configuration\npublic class RestTemplateConfiguration {\n    @Bean\n    public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {\n        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();\n        connectionManager.setMaxTotal(20);\n        connectionManager.setDefaultMaxPerRoute(20);\n        return new HttpComponentsClientHttpRequestFactory(\n                HttpClientBuilder.create()\n                    .setConnectionManager(connectionManager)\n                    .build()\n        );\n    }\n\n    @Bean\n    public RestTemplate restTemplate(RestTemplateBuilder builder, HttpComponentsClientHttpRequestFactory clientHttpRequestFactory) {\n        return builder\n                .requestFactory(() -\u003e clientHttpRequestFactory)\n                .build();\n    }\n}\n----\n\nImportant is the `DefaultMaxPerRoute` configuration. This is 5 by default and specifies the maximum number of connections to one host.\nIf for example the host address is always the same (for example because of server side load balancing), this limits the number of\nconnections independent of the value specified at `MaxTotal`.\n\nThe value of the number of allowed connections in the pool should be configured depending on the required amount and what the server endpoint can handle.\n\nIn order to experiment with this setting you can set the environment variables (docker-compose.yaml):\n\n* `HTTP_CLIENT_USE_DEFAULT_REQUEST_FACTORY=false`\n* `HTTP_CLIENT_POOL_MAX_CONNECTIONS_TOTAL=25`\n* `HTTP_CLIENT_POOL_DEFAULT_MAX_CONNECTIONS_PER_ROUTE=25`\n\nIn addition, it is also important to configure corresponding timeouts and time-to-live to ensure that unused connections are eventually closed\n(especially if a large amount of connections is configured).\n\nLet's return to the `GET /delegate/subcalldemo` calls. If you execute the _curl_ command to this endpoint again, you will\nrecognize, that it's faster now, but still needs ~700ms to complete.\n\n[source,bash]\n----\ntime curl http://localhost:8900/delegate/subcalldemo \u0026\n----\n\nIf you now execute this command every 700ms, it should work without an increase of the duration per request.\n\n[source,bash]\n----\nwhile true; do sleep 0.7; time curl http://localhost:8900/delegate/subcalldemo \u0026; done\n----\n\nHowever, if the interval is increased just a bit, so that the request is executed every 600ms, you will probably notice\nthat the duration continously increases.\n\n[source,bash]\n----\nwhile true; do sleep 0.6; time curl http://localhost:8900/delegate/subcalldemo \u0026; done\n----\n\nThe reason basically is that the maximum workload per second has been achieved.\n\nHowever, with 25 connections in parallel you may have expected that the request returns in `duration = (duration/sub-call * sub-calls) / parallelism`\nwhich is `(0.1s/sub-call * 50sub-calls) / 25 connections = 0.2s`.\n\nLet's adjust the equation to calculate the actual number of parallelism:\n`parallelism = (duration/sub-call * sub-calls) / duration` which is `(0.1s/sub-call * 50sub-calls) / ~0.7s = ~7.14`\n\nThe cause for this simply is, that the thread pool which we used to execute the sub-calls in parallel is limited to 8 threads.\nTherefore, at the moment, increasing the number of max connections to a value higher than 8 has no impact.\n\nIn order to execute the sub-calls in parallel, Spring's `@Async` feature was used:\n\n[source,java]\n----\n@Component\npublic class DemoServiceAdapter {\n    @Async\n    public ListenableFuture\u003cString\u003e getDemoAsync(Duration processDuration) {\n        return new AsyncResult\u003c\u003e(getDemo(processDuration));\n    }\n}\n----\n\nFor execution, behind the scene\nSpring uses a `ThreadPoolTaskExecutor`, which is instantiated by `TaskExecutionAutoConfiguration` class.\n\nHowever, the default configuration of this executor is limited to a maximum number of 8 threads:\n\n[source,yaml]\n----\nspring:\n  task:\n    execution:\n      pool:\n        core-size: 8\n        max-size: 2147483647\n        queue-capacity: 2147483647\n----\n\nYou may wonder why the number of threads is not increased, because the _max-size_ is unlimited.\nThe reason is, that _queue-capacity_ is also unlimited. `ThreadPoolTaskExecutor` only adds new threads if the\nqueue capacity is reached. This is basically also true some other _Executor_ implementations like for example `ThreadPoolExecutor`\nTherefore, _max-size_ and _queue-capacity_ should be configured together.\nFor additional information about thread pools see: link:https://www.baeldung.com/thread-pool-java-and-guava[Introduction to Thread Pools in Java]\n\nLet's come back to the current maximum achievable workload per second:\n`sub-calls = duration * parallelism / duration/sub-call` which is `1s * 8threads / 0.1s = 80 sub-calls/sec`\nwhich in turn corresponds to a maximum of 1.6 requests/sec.\n\nThe expected duration per request is `duration = (duration/sub-call * sub-calls) / parallelism`\nwhich is `(0.1s/sub-call * 50sub-calls) / 8threads = 0.625s`.\n\nThis is nearly exactly what the experiments showed. At the moment, when we execute more than one request within 0.625s\nthe service cannot handle the load anymore. What basically happens is that more tasks are added to the task queue\nfor the `ThreadPoolTaskExecutor` than the executors can process.\n\nNow let's increase the number of threads for the task execution, by setting _max-size_ to 25 and _queue-capacity_ to 100.\nThis can be done by defining the following environment variables (docker-compose-yaml):\n\n* `SPRING_TASK_EXECUTION_POOL_MAX_SIZE=25`\n* `SPRING_TASK_EXECUTION_POOL_QUEUE_CAPACITY=100`\n\nIf you now execute the request again every second, nothing will change. The duration still is ~700ms.\n\n[source,bash]\n----\nwhile true; do sleep 1; time curl http://localhost:8900/delegate/subcalldemo \u0026; done\n----\n\nHowever, if you increase the rate to every 0.5 seconds:\n\n[source,bash]\n----\nwhile true; do sleep 0.5; time curl http://localhost:8900/delegate/subcalldemo \u0026; done\n----\n\nYou will notice that first the duration increases, but than dramatically is reduced to ~350ms.\nThe reason for this is, that by executing the request every second, the task queue size stays below 100.\nIncreasing the rate beyond the workload limit of 1.6 requests/second to 2 requests/second causes the queue to continously grow.\nIf the 100 task limit is reached, an additional thread is created. This is repeated until the maximum thread size is reached,\nor the queue size no longer reaches the configured limit.\nThe latter was the case at our example.\n\nIf the rate in turn is increased to 4 requests/second, it creates all 25 threads and with ~200ms we achieve the expected duration of a request.\n\nIf you play with this demo application, it is possible that a `RejectedExecutionException` is thrown.\nThis happens if the maximum thread count is reached, and the task queue is full. For more details about this behaviour see:\nlink:https://www.baeldung.com/java-rejectedexecutionhandler[Guide to RejectedExecutionHandler].\n\nThis shows that it is possible to increase the possible workload which an application can handle.\nHowever, in order to do this in a proper and sustainable way, a comprehensive observability of the entire application\nis inevitable. The Spring Boot Actuator provides a good starting point. It can expose metrics in the prometheus format,\nlike it is done in this demo application. However, by default not much metrics are exposed.\nBecause actuator uses Micrometer for metrics, this is easily extensible. For most libraries there are already Micrometer\nmetric extensions available. On this demo application, for example additional metrics are enabled for server connections,\nserver requests, client connections, client requests as well as used thread pools.\n\n=== HTTP/1.1 with Spring WebFlux and Netty\n\ntbd\n\n[source,bash]\n----\ntime curl http://localhost:8700/delegate/subcalldemo\n----\n\n=== HTTP/2 with Spring WebFlux and Netty\n\ntbd\n\n[source,bash]\n----\ntime curl --http2-prior-knowledge http://localhost:8700/delegate/subcalldemo\n----\n\n== Next Steps\n\n* More detailed explanation about monitoring and activation of metrics for connection pools, server and thread pools.\n* Short comparison of HTTP/1.1, HTTP/2 and HTTP3\n* Metrics for Netty Event Loop (like utilization)\n* Spring WebFlux: Evaluate incubator HTTP/3 (https://netty.io/news/2021/03/04/http3-0-0-1-Final.html and https://netty.io/news/2020/12/09/quic-0-0-1-Final.html)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnovatecconsulting%2Fshowcase-spring-http","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnovatecconsulting%2Fshowcase-spring-http","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnovatecconsulting%2Fshowcase-spring-http/lists"}