Home | Send Feedback

Real-Time Polling App with Java and JavaScript

Published: February 28, 2018  •  java, javascript

This blog post is based on a Traversy Media video. If you do not already know Traversy, you should check out his content. He publishes a lot of free videos on his YouTube channel, created courses on Udemy, and other resources, visit his homepage to find out more. Great learning resources for full-stack developers.

In this blog post, we're going to create a polling application where users can vote for their favorite operating system and see the vote result in real-time in a pie chart. It's not a one-to-one copy of Traversy's project (although I copied the HTML code) and is using a different technology stack behind the scenes.

Here is a list of all the tools and libraries we are using in this project


Library Homepage
Parcel https://parceljs.org/
echarts https://echarts.apache.org/en/index.html
muicss https://www.muicss.com/
uuid https://www.npmjs.com/package/uuid
Polyfills classlist-polyfill, core-js, event-source-polyfill, whatwg-fetch


Library Homepage
Maven https://maven.apache.org/
Spring https://projects.spring.io/spring-framework/
Spring Boot https://projects.spring.io/spring-boot/
sse-eventbus https://github.com/ralscha/sse-eventbus
MapDB https://mapdb.org/


The visible part of the application is an HTML page consisting of a form where users can vote for their favorite operating system and a pie chart that shows the polling result in real-time overview png

Behind the scenes, we have a JavaScript application that sends the user vote to a Spring Boot server with a POST request. The JavaScript application also opens a server-sent event connection to the server and receives the vote result in real-time as soon as users are voting and then displays the result with a pie chart.

The server is written in Java and is based on Spring and Spring Boot. It handles the vote POST request, stores the poll data in a MapDB database to make it persistent and broadcasts the current vote result to all connected clients via server-sent events.



As usual, I start my Spring Boot application with a visit to the https://start.spring.io page. For this project, we only need the Web dependency.

Then we add the additional dependencies to the pom.xml




Next, we open the main class (I renamed it to Application.java) and enable the SSE event bus with the @EnableSseEventBus annotation. Behind the scenes, this configures a SseEventBus singleton, that we can inject into other Spring-managed beans.

public class Application {


Next, we create a custom resource configuration.

class ResourceConfig implements WebMvcConfigurer {

  private Environment environment;

  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (this.environment.acceptsProfiles(Profiles.of("development"))) {
      String userDir = System.getProperty("user.dir");
          .addResourceLocations(Paths.get(userDir, "../client/dist").toUri().toString())
    else {
          .addResolver(new EncodedResourceResolver());

          .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
          .resourceChain(false).addResolver(new EncodedResourceResolver());

  public void addViewControllers(ViewControllerRegistry registry) {


If the application runs with the "development" profile, the configuration sets the webroot to ../client/dist. This way we don't have to restart the Spring Boot service whenever we change code on the client-side during development.

When we start a production build, the build system bundles the client code into the server JAR file, and for that, we configure the resource handler to look for these resources in the classpath:/static/ folder. The configuration also makes sure that index.html is served with a nocache caching header, and the other resources are sent with a cache-control header with an expiry date one year in the future. The client build system pre-compresses the resources with gzip and brotli, so we add the EncodedResourceResolver that can serve these pre-compressed files. This saves a few server CPU cycles because the server no longer needs to compress resources on the fly.

The configuration also adds a redirect configuration from / to index.html.

See also my blog post about Parcel and Maven.

The main application logic is located in the PollController class.

The constructor of the class creates or opens the MapDB database and initializes the votes with zeros if the database is empty.
MapDB provides not only Maps but also Sets, Lists, and Queues that are backed by off-heap or on-disk storage.
It's a combination between a collection library and a database engine. For this example, we use a Map that is backed by a file (./counter.db) and therefore is persistent and survives a server restart.

    this.db = DBMaker.fileDB("./counter.db").transactionEnable().make();
    this.pollMap = this.db.hashMap("polls", Serializer.STRING, Serializer.LONG)

    for (String os : oss) {
      this.pollMap.putIfAbsent(os, 0L);


The method poll() implements the POST endpoint that handles the user vote. First, it inserts the vote into the database.

  @ResponseStatus(code = HttpStatus.NO_CONTENT)
  public void poll(@RequestBody String os) {
    this.pollMap.merge(os, 1L, (oldValue, one) -> oldValue + one);


After that, the method broadcasts the new result to all connected clients with a server-sent event. The payload of the server-sent event is a simple comma-separated string (e.g. 10,11,4,1), each number represents the current voting total for one of the four operating systems ("Windows", "macOS", "Linux", "Other")

  private void sendPollData(String clientId) {
    StringBuilder sb = new StringBuilder(10);

    for (int i = 0; i < oss.length; i++) {
      if (i < oss.length - 1) {

    SseEvent.Builder builder = SseEvent.builder().data(sb.toString());
    if (clientId != null) {


The register() method implements the endpoint for the initial server-sent event request. The sse-eventbus library requires the client to send a unique id that identifies the client. The method then registers the client and sends the current voting result back to the browser. This way, the client does not have to wait until somebody votes and instead immediately receives the current voting result and can display the pie chart.

  public SseEmitter register(@PathVariable("id") String id,
      HttpServletResponse response) {
    response.setHeader("Cache-Control", "no-store");
    SseEmitter sseEmitter = this.eventBus.createSseEmitter(id, SseEvent.DEFAULT_EVENT);

    // send the initial data only to this client

    return sseEmitter;


For bundling the client and server into one package we use the frontend-maven-plugin and maven-antrun-plugin. You find more information about this setup in my blog post about Parcel and Maven.


The client dependencies are managed by npm. We start this project in an empty directory and call npm init to create the package.json file. Then create a src directory and install the dependencies.

npm install echarts
npm install muicss
npm install uuid

The application should also support older browsers, so we install a few polyfills. If you don't target older browsers, you only need event-source-polyfill because Microsoft Edge browser supports most modern standards except server-sent events.

npm install classlist-polyfill
npm install core-js
npm install event-source-polyfill
npm install whatwg-fetch

The application is going to load the polyfills conditionally with Parcel's built-in support for code splitting. This is a technique I wrote about in this blog post.

Create a new JavaScript file that just imports the polyfills

import { EventSourcePolyfill } from 'event-source-polyfill';
import "whatwg-fetch";

window.EventSource = EventSourcePolyfill;


The entry point of the application (main.js) checks if the browser supports all necessary functions natively. If not, it loads polyfill.js dynamically into the browser and then starts the application (init()).

import {init} from './app';
import "core-js/modules/es.promise";
import "core-js/modules/es.array.iterator";

if (browserSupportsAllFeatures()) {
} else {
    import('./polyfill').then(() => init()).catch(e => console.log(e));

function browserSupportsAllFeatures() {
    return window.EventSource && window.fetch;


The main application method checks if the user has already voted. It does that by checking for an entry in localStorage. This is a very rudimentary check and not recommended for real-world usage. An experienced user can simply delete this entry in the storage with the browser developer tools.

If the user has already voted, the GUI displays a piece of information that he has already voted. If not, the application presents the operating system selection.
The init() method then registers a click listener on the vote button. The handler sends the user choice with a POST request to the /poll endpoint and inserts the hasVoted entry into localStorage.

echarts.use([PieChart, TooltipComponent, TitleComponent, CanvasRenderer]);
const oss = ["Windows", "macOS", "Linux", "Other"];

export function init() {

    const alreadyVoted = localStorage.getItem('hasVoted');
    document.getElementById('hasVotedAlreadyErrorMsg').classList.toggle('hidden', !alreadyVoted);
    document.getElementById('vote-form').classList.toggle('hidden', alreadyVoted);

    const voteButton = document.getElementById('vote-button');

    voteButton.addEventListener('click', e => {
        localStorage.setItem('hasVoted', true)
        const choice = document.querySelector('input[name=os]:checked').value;

        fetch('poll', {
            method: 'POST',
            body: choice
        }).then(() => {


The next part of the init() method configures the pie chart, starts the server-sent events connection with new EventSource(...) and registers a message listener for incoming events.
The handler splits the comma-separated string coming from the server, calculates the total, and updates the chart.

    const chart = echarts.init(document.getElementById('chart'));

    const eventSource = new EventSource(`register/${uuidv4()}`);
    eventSource.addEventListener('message', response => {
        const pollData = response.data.split(',').map(Number);
        const total = pollData.reduce((accumulator, currentValue) => accumulator + currentValue);

            title: {
                text: `Total Votes: ${total}`
            series: {
                data: [
                    { value: pollData[0], name: oss[0] },
                    { value: pollData[1], name: oss[1] },
                    { value: pollData[2], name: oss[2] },
                    { value: pollData[3], name: oss[3] }



The CSS file only contains two rules. The primary purpose is to import the MUI CSS library. This is a lightweight CSS framework that implements Google's Material design.

@import '../node_modules/muicss/dist/css/mui.min.css';

body {
  font-family: "Roboto", "Helvetica Neue", Helvetica, Arial;

.hidden { 
  display: none; 


This is already everything we need to implement for the client application.

As mentioned above, we use Parcel for building and bundling the application. First, we need to install it

npm install -D parcel@next

We also install shx to get support for platform-independent shell commands and bread-compressor-cli a command line tool that compresses resources with brotli and zopfli (gzip).

npm install shx -D
npm install bread-compressor-cli -D

Out of the box, Parcel already transpiles our code into ECMAScript 5 code with Babel. We don't have to configure anything. We can configure the target platforms with a browserlist query in package.json.

  "browserslist": [
    "> 1%",
    "last 2 versions"


Then we add three scripts to the scripts section of the package.json file.

  "scripts": {
    "prebuild": "shx rm -rf dist/*",
    "build": "parcel build src/index.html --public-url ./",
    "postbuild": "bread-compressor dist",
    "watch": "parcel watch src/index.html --public-url ./"


The prebuild script deletes everything from the dist folder before npm runs the build task.
The watch script is used during development where Parcel starts a file watcher and rebuilds changed resources on the fly.

The watch option only starts a file watcher. It does not start an HTTP server like the parcel src/index.html command. Because we serve the content with the Spring Boot application, there is no need for an additional web server during development.
Serving the client application through Spring Boot has the advantage that we don't need a CORS configuration. This only works if you are planning to install the client and server on the same origin.

If you want to install the client and server on different origins (different protocol, host, or port), you need to enable CORS on the PollController.

public class PollController {

If everything is in place, you can test the application.
First, build the client with the watch command from the client directory.

npm run watch

Then start the server from the server directory

.\mvnw.cmd spring-boot:run -Dspring.profiles.active=development

If you are working with macOS or Linux, start Maven with the ./mvnw.sh script.

Open the browser with the URL http://localhost:8080, and you should see the application.

For a production build, issue this command from the server directory.

.\mvnw.cmd clean package

This command automatically starts the npm run build command in the client folder and copies the client code from the client/dist folder into the jar file.

If the build was successful, start the application with

java -jar target\poll-0.0.1.jar

Test the application with the URL http://localhost:8080

You find the complete source code for the project on GitHub: