{"id":15699026,"url":"https://github.com/pgilad/spring-boot-demo","last_synced_at":"2026-05-06T01:36:03.914Z","repository":{"id":66262793,"uuid":"149122685","full_name":"pgilad/spring-boot-demo","owner":"pgilad","description":"A guide to understanding Spring Boot WebFlux with Reactive Mongo","archived":false,"fork":false,"pushed_at":"2018-10-09T11:39:00.000Z","size":78,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-05T16:15:33.282Z","etag":null,"topics":["demo","lombok","mongodb","reactive","reactor3","spring-boot","starter","webflux"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pgilad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2018-09-17T12:35:21.000Z","updated_at":"2018-12-12T14:07:38.000Z","dependencies_parsed_at":"2023-07-09T02:30:43.552Z","dependency_job_id":null,"html_url":"https://github.com/pgilad/spring-boot-demo","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/pgilad%2Fspring-boot-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgilad%2Fspring-boot-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgilad%2Fspring-boot-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgilad%2Fspring-boot-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pgilad","download_url":"https://codeload.github.com/pgilad/spring-boot-demo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246329589,"owners_count":20759927,"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":["demo","lombok","mongodb","reactive","reactor3","spring-boot","starter","webflux"],"created_at":"2024-10-03T19:37:30.398Z","updated_at":"2025-10-20T01:14:48.502Z","avatar_url":"https://github.com/pgilad.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spring Boot Webflux With Reactive Mongo\n\n\u003e A guide to bootstrapping a basic Spring Boot Webflux Application, with Reactive Mongo\n\n  * [Prerequisites](#prerequisites)\n  * [Setup](#setup)\n  * [Hello World](#hello-world)\n  * [Adding logs](#adding-logs)\n  * [Changing the port](#changing-the-port)\n  * [Health checks](#health-checks)\n  * [Info APIs](#info-apis)\n  * [Reactive Vs. Imperative, or why Reactive?](#reactive-vs-imperative-or-why-reactive)\n  * [Adding Reactive Mongo](#adding-reactive-mongo)\n  * [Validation](#validation)\n  * [Last play - Server Sent Events (SSE)](#last-play---server-sent-events-sse)\n  * [Where to go from here](#where-to-go-from-here)\n\n## Prerequisites\n\n- IntelliJ IDEA 2018.2.3 (Ultimate Edition) or newer\n- Gradle 4.10.1 or newer (Use Sdkman to install)\n- Java 10.0.2 or newer (Use Sdkman to install)\n- Lombok plugin for IntelliJ\n- `git`\n- `httpie` (`brew install httpie`) (Optional)\n- `mongodb` for later\n\n## Setup\n\n- [https://start.spring.io/](https://start.spring.io/)\n- Gradle, Java 10\n- Add dependencies:\n  - Reactive Web\n  - Actuator\n  - DevTools\n  - Lombok\n- Download \u0026 unzip archive into ~/repos/spring-boot-demo\n- Start IntelliJ\n- Setup + Add `Annotation Processors -\u003e Enable annotation processing`\n- Run application (*hint*: currently it does nothing)\n\n## Hot Tips\n\n- Enable IntelliJ Auto-Import of dependencies (Editor -\u003e General -\u003e Auto Import -\u003e Add unambiguous imports on the fly\n- Copy-Paste code directly into package (project explorer) to create component automatically !!\n\n## Hello World\n\nLet's add a simple controller with mapping to respond to an `hello` request from the client:\n\nCreate a new file named `HelloController.java`:\n\n```java\n@RestController\npublic class HelloController {\n\n    @GetMapping(\"/hello\")\n    public Mono\u003cString\u003e sayHello() {\n        return Mono.just(\"Hello\");\n    }\n}\n```\n\nRestart the application, and now let's get our first response back from the server:\n\n```bash\n$ http localhost:8080/hello\nHTTP/1.1 200 OK\nContent-Length: 6\nContent-Type: text/plain;charset=UTF-8\n\nHello\n```\n\nWe have a simple Hello World application. :smile:\n\n## Adding logs\n\nAnnotate any class with `@Sl4f` and add a log:\n\n`log.info(\"Here\");`\n\nThis uses `Lombok` which does annotation processing which re-writes your source code behind the screens.\n\nThis actually adds a line like this to your class:\n\n`private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);`\n\nWe will see how `Lombok` will actually aid us in saving a lot of boilerplate code (Which Java is known for).\n\n## Changing the port\n\nThe easiest way to change the port, is by overriding `server.port` in `application.properties`:\n\n`server.port=9090`\n\nGo ahead and re-run your application, now it runs on `9090`.\n\n## Health checks\n\nA basic health check is setup due to `spring-boot-actuator`. Let's give it a test:\n\n```bash\n$ http localhost:9090/actuator/health\nHTTP/1.1 200 OK\nContent-Length: 15\nContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\n\n{\n    \"status\": \"UP\"\n}\n```\n\nWe can easily extend this health check to include our custom health checks:\n\nAdd another file `health/CustomHealthCheck.java`:\n\n```java\n@Component\npublic class CustomHealthCheck implements ReactiveHealthIndicator {\n\n    @Override\n    public Mono\u003cHealth\u003e health() {\n        return Mono.just(Health.up().withDetail(\"Service\", \"Good!\").build());\n    }\n}\n```\n\nIn order to see full-details on health API, we need to add the following to our `application.properties`:\n\n```ini\nmanagement.endpoint.health.show-details=ALWAYS\n```\n\nNow let's try our health API again:\n\n```bash\n$ http localhost:9090/actuator/health\nHTTP/1.1 200 OK\nContent-Length: 195\nContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\n\n{\n    \"details\": {\n        \"customHealthCheck\": {\n            \"details\": {\n                \"Service\": \"Good!\"\n            },\n            \"status\": \"UP\"\n        },\n        \"diskSpace\": {\n            \"details\": {\n                \"free\": 190510637056,\n                \"threshold\": 10485760,\n                \"total\": 499963174912\n            },\n            \"status\": \"UP\"\n        }\n    },\n    \"status\": \"UP\"\n}\n```\n\n## Info APIs\n\nLet's assume we want to show various information about our app. This is revealed in the following `actuator` API:\n\n```bash\n$ http localhost:9090/actuator/info\nHTTP/1.1 200 OK\nContent-Length: 2\nContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\n\n{}\n```\n\nCurrently we are exposing nothing. But let's add some general info. Add the following to your `application.properties`:\n\n```ini\ninfo.coolness.spring=agile\n```\n\nIf we run the same `httpie` request now:\n\n```bash\n$ http localhost:9090/actuator/info\nHTTP/1.1 200 OK\nContent-Length: 31\nContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\n\n{\n    \"coolness\": {\n        \"spring\": \"agile\"\n    }\n}\n```\n\nThat was easy. Now let's add some info with logic:\n\nAdd a new file named `info/InfoContrib.java`:\n\n\n```java\n@Component\npublic class InfoContrib implements InfoContributor {\n\n    @Override\n    public void contribute(Info.Builder builder) {\n        builder.withDetail(\"contributor\", \"foo\");\n    }\n}\n```\n\nNow when we run a request:\n\n```bash\n$ http localhost:9090/actuator/info\nHTTP/1.1 200 OK\nContent-Length: 51\nContent-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8\n\n{\n    \"contributor\": \"foo\",\n    \"coolness\": {\n        \"spring\": \"agile\"\n    }\n}\n```\n\nAnd off course there are easy ways to auto add application information (such as git, version, etc...)\n\nEnough with all this setup, it's cool, but where's the real stuff?\n\n## Reactive Vs. Imperative, or why Reactive?\n\nLet's implement a simple dictionary word count algorithm, twice as imperative (with naive assumptions) and once as reactive.\nReactive is meant for observable and async operations, but it's also awesome for regular stream manipulations because of the operators.\n\nFirst, our basic story, which we'll extract the top N word count frequencies, with words bounded by a space, and lowercase.\n\nWe'll implement this in our `HelloController`, but you can use any other controller as well.\n\n```java\nprivate static String story = \"the quick brown fox jumped over the lazy fence and then noticed another quick black fox \" +\n            \"that was much quicker than the original fox but the original fox was able to jump higher over the fence\";\n```\n\nFirst, our initial naive implementation:\n\n```java\n@GetMapping(\"/word-count/v1\")\npublic Flux\u003cTuple2\u003cString, Long\u003e\u003e wordCount1(@RequestParam(defaultValue = \"2\") Integer limit) {\n    String[] words = story.split(\" \");\n    HashMap\u003cString, Long\u003e counts = new HashMap\u003c\u003e();\n    for (String word : words) {\n        Long count = counts.getOrDefault(word, 0L) + 1;\n        counts.put(word.toLowerCase(), count);\n    }\n    Set\u003cLong\u003e values = new HashSet\u003c\u003e(counts.values());\n    final ArrayList\u003cLong\u003e sortedCounts = new ArrayList\u003c\u003e(values);\n    sortedCounts.sort(Collections.reverseOrder());\n\n    ArrayList\u003cTuple2\u003cString, Long\u003e\u003e response = new ArrayList\u003c\u003e();\n    for (var i = 0; i \u003c Math.min(limit, sortedCounts.size()); i++) {\n        Long count = sortedCounts.get(i);\n        for (Map.Entry\u003cString, Long\u003e entry : counts.entrySet()) {\n            if (entry.getValue().equals(count)) {\n                response.add(Tuples.of(entry.getKey(), count));\n            }\n        }\n    }\n\n    return Flux.fromIterable(response);\n}\n```\n\nLet's verify that it works: `http :9090/word-count/v1`. You should see a list of tuples of words and their frequency.\n\nNow, our second improved implementation using `TreeMap`. The details here don't really matter, as we're not comparing performance, but ease of implementations.\n\n```java\n@GetMapping(\"/word-count/v2\")\npublic Flux\u003cTuple2\u003cString, Long\u003e\u003e wordCount2(@RequestParam(defaultValue = \"2\") Integer limit) {\n    String[] words = story.split(\" \");\n\n    HashMap\u003cString, Long\u003e counts = new HashMap\u003c\u003e();\n    TreeMap\u003cString, Long\u003e sortedCounts = new TreeMap\u003cString, Long\u003e(Comparator.comparing(counts::get, Comparator.reverseOrder()));\n\n    for (String word : words) {\n        counts.merge(word.toLowerCase(), 1L, Math::addExact);\n    }\n    sortedCounts.putAll(counts);\n    ArrayList\u003cTuple2\u003cString, Long\u003e\u003e response = new ArrayList\u003c\u003e();\n    for (var i = 0; i \u003c limit; i++) {\n        Map.Entry\u003cString, Long\u003e entry = sortedCounts.pollFirstEntry();\n        if (entry == null) {\n            // no more entries in map\n            break;\n        }\n        response.add(Tuples.of(entry.getKey(), entry.getValue()));\n    }\n\n    return Flux.fromIterable(response);\n}\n```\n\nNow finally, let's see how it's done using Reactive Streams (1 possible solution):\n\n```java\n@GetMapping(\"/word-count/v3\")\npublic Flux\u003cTuple2\u003cString, Long\u003e\u003e wordCount3(@RequestParam(defaultValue = \"2\") Integer limit) {\n    return Flux.fromArray(story.split(\" \"))\n            .map(String::toLowerCase)\n            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))\n            .flatMapIterable(Map::entrySet)\n            .sort((a, b) -\u003e b.getValue().compareTo(a.getValue()))\n            .map(a -\u003e Tuples.of(a.getKey(), a.getValue()))\n            .take(limit);\n}\n```\n\nSweet right? Now imagine you need to do async, or multi-threading, or timeouts or buffering. Adding that to the reactive impl. is as easy as adding a new line.\nAdding those to the imperative impl. is hard.\n\n## Adding Reactive Mongo\n\nAdd the following line to your `build.gradle` under `dependencies`:\n\n`compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')`\n\nAnd let's add the annotations to support Reactive Mongo:\n\n```java\n\n```\n\n`@EnableReactiveMongoRepositories` and `@EnableMongoAuditing` under main app.\n\nLet's create our domain document, `Project`:\n\n```java\n@Document\n@Data\npublic class Project {\n\n    @Id\n    private String id;\n\n    private String name;\n\n    private String description;\n\n    @CreatedDate\n    private LocalDateTime createdAt;\n}\n```\n\n`@Document` is a mongo annotation, while `@Data` is a magic `Lombok` annotation which auto creates lots of stuff for us\n(getters, setters, all args constructor and some other magic).\n\nEverything is now in place to wire up our controller with mongo.\n\nFirst let's create a repository interface, almost everything we need to work with mongo (or reactive mongo in our case):\n\n`repository/ProjectMongoRepository.java`:\n\n```java\n@Repository\npublic interface ProjectMongoRepository extends ReactiveMongoRepository\u003cProject, String\u003e {\n}\n```\n\nLet's create our very own `controller/ProjectController.java`:\n\n```java\n@RestController\n@RequestMapping(\"/api/projects\")\npublic class ProjectsController {\n\n    private final ProjectMongoRepository mongo;\n\n    @Autowired\n    public ProjectsController(ProjectMongoRepository mongo) {\n        this.mongo = mongo;\n    }\n\n    @GetMapping\n    private Flux\u003cProject\u003e getProjects() {\n        return mongo.findAll();\n    }\n\n    @PostMapping\n    private Mono\u003cProject\u003e createProject(@RequestBody @Valid Project project) {\n        return mongo.save(project);\n    }\n\n    @GetMapping(\"/{id}\")\n    private Mono\u003cResponseEntity\u003cProject\u003e\u003e getProject(@PathVariable String id) {\n        return mongo.findById(id)\n                .map(ResponseEntity::ok)\n                .defaultIfEmpty(ResponseEntity.notFound().build());\n    }\n\n    @PutMapping(\"/{id}\")\n    private Mono\u003cResponseEntity\u003cProject\u003e\u003e updateProject(@PathVariable String id, @RequestBody Project project) {\n        return mongo.findById(id)\n                .flatMap(existingProject -\u003e {\n                    existingProject.setName(project.getName());\n                    existingProject.setDescription(project.getDescription());\n\n                    return mongo.save(existingProject);\n                })\n                .map(ResponseEntity::ok)\n                .defaultIfEmpty(ResponseEntity.notFound().build());\n    }\n\n    @DeleteMapping(\"/{id}\")\n    private Mono\u003cResponseEntity\u003cObject\u003e\u003e deleteProject(@PathVariable String id) {\n        return mongo.findById(id)\n                .flatMap(project -\u003e {\n                    return mongo\n                            .delete(project)\n                            .thenReturn(ResponseEntity.noContent().build());\n                })\n                .defaultIfEmpty(ResponseEntity.notFound().build());\n    }\n}\n```\n\nThis is a pretty simple CRUD example using Reactive Mongo. Notice how all of the Mongo operations:\n\n1. Are already defined by our Repository interface (and ready to use)\n2. Return a `Flux | Mono`\n\nThis means that we can run any stream operation on them because they act as an Publisher.\n\nOne thing that comes to mind, is what about validation? How do we ensure that the client sends us the expected data?\n\nWell, we already handle missing entities, but we haven't handled properties validation. (Permissions and security will be on a different guide).\n\n## Validation\n\nLet's decide that `project.name` cannot be null or an empty string. This makes sense as we don't want to save projects without names\nWe do allow empty `project.description` though, but we want to limit it to maximum of 100 characters. We also want to limit\n`project.name` length to 50 characters.\n\nThis is easily added using `JSR-303` bean validation, already included in Spring Boot Web(flux).\n\nLet's revisit our `Project` model:\n\n```java\n@Data\n@Document\nclass Project {\n\n    @Id\n    private String id;\n\n    @NotBlank\n    @Length(min = 1, max = 30)\n    private String name;\n\n    @Length(max = 100)\n    private String description;\n\n    @CreatedDate\n    @JsonProperty(access = JsonProperty.Access.READ_ONLY)\n    private LocalDateTime createdAt;\n}\n```\n\nNotice how we add annotation over the fields. This annotation based validation makes our life very easy. We can also\ncreate custom validations (using annotations) but we'll leave that for another lesson.\n\nNotice also how we added `@JsonProperty(access = JsonProperty.Access.READ_ONLY)` to the `createdAt` field - this ensures\nthat this field is only serialized (when being field is being read) but never when writing to class. This prevents the user\nfrom feeding his own `createdAt` data, interfering our mongo auditing.\n\nOne last thing we will need to add is a proper error validation formatting (the default is way too verbose). Add the following\nmethod to the `ProjectsController`:\n\n```java\n@ExceptionHandler(WebExchangeBindException.class)\n@ResponseStatus(HttpStatus.BAD_REQUEST)\npublic Mono\u003cList\u003cString\u003e\u003e handleWebExchangeBindException(WebExchangeBindException e) {\n    return Flux\n            .fromIterable(e.getFieldErrors())\n            .map(field -\u003e String.format(\"%s.%s %s\", e.getObjectName(), field.getField(), field.getDefaultMessage()))\n            .collectList();\n}\n```\n\nAnd that's it. Give it a try with:\n\n```bash\n$ http POST :9090/api/projects/ name='' description='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'\nHTTP/1.1 400 Bad Request\nContent-Length: 136\nContent-Type: application/json;charset=UTF-8\n\n[\n    \"project.name length must be between 1 and 30\",\n    \"project.description length must be between 0 and 100\",\n    \"project.name must not be blank\"\n]\n```\n\nYep we got sweet validation.\n\n## Last play - Server Sent Events (SSE)\n\nLet's add another method to our controller, that can stream projects back to the client:\n\n```java\n@GetMapping(value = \"/stream\", produces = MediaType.APPLICATION_STREAM_JSON_VALUE)\nprivate Flux\u003cProject\u003e getProjectsStream() {\n    return mongo.findAll().delayElements(Duration.ofSeconds(1));\n}\n```\n\nWe simulate a delay in responding to the client. In order to request the data make sure you client is not waiting for the stream to end, for example:\n\n```bash\n$ http -S :9090/api/projects/stream\n```\n\nAnd that's how we added SSE support!\n\n## Where to go from here\n\nWe have only just glimpsed the surface of Spring Boot. Off course there are many other modules and extension to it.\nSeveral key concepts come to mind:\n\n1. Security (`spring-boot-security`)\n2. Permissions\n3. Pagination\n4. HATEOS\n5. Testing!!\n6. Packaging and releasing\n7. Dockerizing the app\n8. Monitoring (and metrics)\n9. Scale\n10. Configuration, development and production\n\nMuch much more...\n\nKeep learning and having fun, and share your success (or frustrations) with Spring Boot!!!\n\n## License\n\nMIT\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgilad%2Fspring-boot-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpgilad%2Fspring-boot-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgilad%2Fspring-boot-demo/lists"}