Voting System

Overview

Voting system developed with Spring Boot, Redis and Angular (Clarity) frontend.

Github: https://github.com/gitorko/project94

Quick Overview

To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run

1git clone https://github.com/gitorko/project94
2cd project94
3docker-compose -f docker/docker-compose.yml up 

Open http://localhost:8080/

Features

Users should be able to vote for candidates.

The same solution can be extended to the following systems

  1. metering - subscription usage, consumption capping & pricing etc
  2. rate limiting - Counter with TTL, Token Bucket, Leaky Bucket, Sliding window counter
  3. Prevent denial of service (DoS)
  4. Traffic shaping
  5. Live visitor/user count
  6. Like and dislike count

Functional Requirements

  1. An active/live voting system shows the live count of the votes as they are cast.
  2. The running count should be accurate with no race conditions.
  3. Storing of votes is not required, objective is just to track live counts. Who voted to which candidate information need not be stored.
  4. Only 2 candidates in the voting system, cats vs dogs.
  5. The display should show the live count of votes as they are cast without having the user refresh each time.
  6. Display must provide UI to vote for candidates, as well as support api based voting.

Non-Functional Requirements

  1. Latency should be low.
  2. System should be highly available.
  3. System should scale well when number of users increases
  4. Handle concurrent request and counter value consistent.

Future

  1. The design can further be modified to use write-back cache to write the running counter to the database. This way we avoid loosing the votes in case redis server goes down. Redis supports AOF (append-only file), which copies write commands to disk as they happen, and snapshotting, which takes the data as it exists at one moment in time and writes it to disk
  2. The votes can be persisted to the db by using a queuing mechanism. This will persist the who voted for whom information. We use a queue to keep the latency low. As soon as the vote counter is increased the vote object is queued and a consumer service will dequeue the request and persist to the db.
  3. Authentication and user tracking can be added.
  4. The project can be changed to spring reactor to make use of non blocking framework.
  5. Unsubscribe flow needs to be handled when browser is closed

Design

  1. We will use Redis to count the votes, this will help us scale well. The counter increment needs to be atomic in nature. Redis provides this feature out of the box, where there is less contention among threads when updating atomic long.
  2. We will not persist the votes to a database as the objective is to keep an active running counter. Adding a database in the synchronous call introduces latency which prevent scaling the application.
  3. The backend and frontend bundle into a single uber jar that can be deployed on many servers there by providing ability to horizontally scale.
  4. We will use SSE (server sent events) to stream the voting results to the app. This way the live counter will always be displayed.
  5. We will use angular clarity for the UI

Redis is an open-source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams. Redis has built-in data structures, atomic commands, and time-to-live (TTL) capabilities that can be used to power metering use cases. Redis runs on a single thread. Therefore, all of the database updates are serialized, enabling Redis to perform as a lock-free data store. This simplifies the application design as developers don’t need to spend any effort on synchronizing the threads or implementing locking mechanisms for data consistency. Redis stores integers as a base-10 64-bit signed integer. Therefore the maximum limit for an integer is a very large number: 263 – 1 = 9,223,372,036,854,775,807. To understand the problem with a counter on multi-thread environment refer AtomicLong vs LongAdder

Code

 1package com.demo.project94.controller;
 2
 3import java.util.concurrent.ExecutorService;
 4import java.util.concurrent.Executors;
 5import java.util.concurrent.TimeUnit;
 6
 7import lombok.extern.slf4j.Slf4j;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.data.redis.core.RedisTemplate;
10import org.springframework.http.HttpStatus;
11import org.springframework.http.MediaType;
12import org.springframework.http.ResponseEntity;
13import org.springframework.web.bind.annotation.DeleteMapping;
14import org.springframework.web.bind.annotation.GetMapping;
15import org.springframework.web.bind.annotation.PathVariable;
16import org.springframework.web.bind.annotation.PostMapping;
17import org.springframework.web.bind.annotation.RestController;
18import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
19
20@RestController
21@Slf4j
22public class HomeController {
23
24    @Autowired
25    private RedisTemplate<String, Long> redisTemplate;
26
27    private ExecutorService executor = Executors.newCachedThreadPool();
28
29    @PostMapping(value = "/api/vote/{id}")
30    public Long vote(@PathVariable String id) {
31        log.info("voting for {}", id);
32        return redisTemplate.opsForValue().increment(id);
33    }
34
35    @DeleteMapping(value = "/api/vote/{id}")
36    public void resetVote(@PathVariable String id) {
37        redisTemplate.opsForValue().getAndDelete(id);
38    }
39
40    @GetMapping(value = "/api/votes", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
41    public ResponseEntity<SseEmitter> getVotes() {
42        SseEmitter emitter = new SseEmitter(15000L);
43        executor.execute(() -> {
44            try {
45                int id = 0;
46                while (true) {
47                    SseEmitter.SseEventBuilder event = SseEmitter.event()
48                            .data("cat: " + redisTemplate.opsForValue().get("cat") + "," +
49                                    "dog: " + redisTemplate.opsForValue().get("dog"))
50                            .id(String.valueOf(id++));
51                    emitter.send(event);
52                    TimeUnit.SECONDS.sleep(2);
53                }
54            } catch (Exception ex) {
55                emitter.completeWithError(ex);
56            }
57        });
58        return new ResponseEntity(emitter, HttpStatus.OK);
59    }
60}
 1package com.demo.project94.config;
 2
 3import org.springframework.context.annotation.Bean;
 4import org.springframework.context.annotation.Configuration;
 5import org.springframework.data.redis.connection.RedisConnectionFactory;
 6import org.springframework.data.redis.core.RedisTemplate;
 7import org.springframework.data.redis.serializer.StringRedisSerializer;
 8
 9@Configuration
10public class RedisConfiguration {
11
12    @Bean
13    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
14        RedisTemplate<?, ?> template = new RedisTemplate<>();
15        template.setConnectionFactory(connectionFactory);
16        template.setDefaultSerializer(new StringRedisSerializer());
17        return template;
18    }
19
20}

Setup

Project 94

Voting System

https://gitorko.github.io/voting-system/

Version

Check version

1$java --version
2openjdk 17.0.3 2022-04-19 LTS
3
4node --version
5v16.16.0
6
7yarn --version
81.22.18

Redis

1docker run --name my-redis -p 6379:6379 -d redis redis-server --requirepass "password"

Dev

To run the backend in dev mode.

1./gradlew clean build
2./gradlew bootRun

To Run UI in dev mode

1cd ui
2yarn install
3yarn build
4yarn start

Open http://localhost:4200/

Prod

To run as a single jar, both UI and backend are bundled to single uber jar.

1./gradlew cleanBuild
2cd build/libs
3java -jar project94-1.0.0.jar

Open http://localhost:8080/

Docker

1./gradlew cleanBuild
2docker build -f docker/Dockerfile --force-rm -t project94:1.0.0 .
3docker images |grep project94
4docker tag project94:1.0.0 gitorko/project94:1.0.0
5docker push gitorko/project94:1.0.0
6docker-compose -f docker/docker-compose.yml up

Testing

To reset the votes

1curl --request DELETE 'http://localhost:8080/api/vote/dog'
2curl --request DELETE 'http://localhost:8080/api/vote/cat'

To vote

1curl --request POST 'http://localhost:8080/api/vote/cat'
2curl --request POST 'http://localhost:8080/api/vote/cat'

JMeter

Open the jmx file with Jmeter. Run the test that simulate a 10K concurrent votes and check the throughput.

Voting System JMX

References

https://jmeter.apache.org/

https://www.infoworld.com/article/3230455/how-to-use-redis-for-real-time-metering-applications.html

https://www.infoworld.com/article/3230455/how-to-use-redis-for-real-time-metering-applications.html?page=2

https://redis.io/

comments powered by Disqus