An online voting system is a software platform that allows people to cast their votes electronically with intuitive and secure online interfaces. These systems leverage cutting-edge technologies to facilitate the entire voting process. Online voting systems come in various forms, from secure web portals to mobile applications.
Online voting systems shine in situations where accessibility is a challenge, enabling individuals who might face mobility issues or are geographically distant to participate seamlessly. Moreover, online voting systems are invaluable in large-scale elections, expediting the voting process and significantly reducing the time and resources required for manual counting.
How to Follow Along
You can grab the source code from our repo:
`$ git clone https://github.com/alifruliarso/spring-boot-blog-voting.git –branch voting-system
Where is online voting most useful?
It’s a good idea to use an online voting system to:
- Vote on rules and regulations by policy decisions
- Select award nomination
- Gather anonymous feedback from employees.
What We’re Building
In this guide, we will embark on a journey to create a simple voting up and down system using Spring Boot, and NoSQL, and packaging the application into a Docker container. We aim to empower users to express their opinions on blog posts, fostering a vibrant community of discussion and feedback.
Requirements
Based on the previous overview, we will have the following functional requirements:
- Create a blog in the simple form
- Cast votes (upvote or downvote) on blog posts. No login/logout, we will generate random users
- Visualize vote results in a static and real-time chart
The out of scope: * Reverse/change the votes * Change users * User management (register, login, etc) * Blog management (create, edit, delete)
The database
The choice between SQL and NoSQL databases depends on specific requirements. However, here are some reasons why NoSQL databases are preferred over SQL databases for this system:
- Horizontal scalability: NoSQL databases typically provide easy horizontal scalability.
- Query Structure: In this case, there is no need for complex relational operations such as table joins.
- NoSQL databases are typically built with a distributed and decentralized architecture, enabling easy scaling by adding more nodes to the cluster.
Project Setup
What you need to install
- Java 17 or later, Maven 3.5+, Docker engine, and your favorite text editor (Intellij IDEA, or VSCode)
Create a Spring Boot Project
Spring boot offers a fast way to build applications. Spring Boot does not generate code to make edits to your files. Instead, when you start your application, Spring Boot dynamically wires up beans and settings and applies them to your application context. With Spring Boot, we can focus more on the business features and less on the infrastructure.
Navigate to start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup. Click generate, it will generate the Spring Boot project and download it as a zip. Now unzip this project and import it into any IDE.
To interact with GridDB, we need to add a GridDB Java Client to this project. Add the following dependency into maven pom.xml
. xml
Database design
Now, let’s write down the database schema according to the functional requirements. We will have the following entity classes:
-
User
This holds the user’s information in the system. It has the following attributes:
String id
: System generated unique identifier. It is the primary key.String email
String fullName
-
Blog
This entity stores the blog posts. It has the following attributes:
String id
: System generated unique identifier. It is the primary key.String title
: The title of the blog postInteger voteUpCount
: The count of vote upInteger voteDownCount
: The count of votes downDate createdAt
: System-generated timestamp when the blog post created
-
VoteMetrics
This entity captures the voting history. It has the following attributes:
Date timestamp
: System generated. It is the primary key for the time series container (table).String blogId
: The blog ID voted by the userString userId
: The user ID makes votingInteger voteType
: The voting type, 1: Vote Up, 0: Vote Down
Data access with GridDB
First, we create Java POJO classes that represent the underlying table or container in GridDB. We annotate a class with Lombok @Data, which automatically generates getters for all fields, a useful toString method, and hashCode and equals implementations that check all non-transient fields. Also generate setters for all non-final fields, as well as a constructor.
We will create the data access class according to the previous database design.
@Data
public class User {
@RowKey
String id;
String email;
String fullName;
Date createdAt;
}
@Data
public class Blog {
@RowKey
String id;
String title;
Integer voteUpCount;
Integer voteDownCount;
Date createdAt;
}
@Data
public class VoteMetrics {
@RowKey
Date timestamp;
String blogId;
String userId;
Integer voteType;
}
Next, we create the GridDBConfig
class as a central configuration for database operation. The class will do the following: * Read environment variables for connecting to the GridDB database * Create a GridStore class for managing database connection to the GridDB instance * Create GridDB Collection’s container (Table) to manage a set of rows. The container is a rough equivalent of the table in a relational database. * On creating/updating the Collection we specify the name and object corresponding to the column layout of the collection. Also for each collection, we add an index for a column that is frequently searched and used in the condition of the WHERE section of TQL.
@Configuration
public class GridDBConfig {
@Value("${GRIDDB_NOTIFICATION_MEMBER}")
private String notificationMember;
@Value("${GRIDDB_CLUSTER_NAME}")
private String clusterName;
@Value("${GRIDDB_USER}")
private String user;
@Value("${GRIDDB_PASSWORD}")
private String password;
@Bean
public GridStore gridStore() throws GSException {
// Acquiring a GridStore instance
Properties properties = new Properties();
properties.setProperty("notificationMember", notificationMember);
properties.setProperty("clusterName", clusterName);
properties.setProperty("user", user);
properties.setProperty("password", password);
GridStore store = GridStoreFactory.getInstance().getGridStore(properties);
return store;
}
@Bean
public Collection<String, User> userCollection(GridStore gridStore) throws GSException {
Collection<String, User> collection = gridStore.putCollection("users", User.class);
collection.createIndex("email");
return collection;
}
@Bean
public Collection<String, Blog> blogCollection(GridStore gridStore) throws GSException {
Collection<String, Blog> collection = gridStore.putCollection("blogs", Blog.class);
collection.createIndex("title");
return collection;
}
@Bean
public TimeSeries<votemetrics> voteMetricContainer(GridStore gridStore) throws GSException {
TimeSeries</votemetrics><votemetrics> timeSeries = gridStore.putTimeSeries(Constant.VOTEMETRICS_CONTAINER, VoteMetrics.class);
timeSeries.createIndex("blogId");
timeSeries.createIndex("userId");
return timeSeries;
}
}</votemetrics>
Service class
Next, we create a service class with @Service annotation for handling business logic and convert the Entity class into DTO.
BlogService.java
is responsible for finding, and creating blog posts in GridDB.
create
: Creating a new blog.
public Blog create(CreateBlogRequest createBlogRequest) {
Blog blog = new Blog();
blog.setId(KeyGenerator.next("bl"));
blog.setTitle(createBlogRequest.getTitle());
blog.setVoteDownCount(0);
blog.setVoteUpCount(0);
blog.setCreatedAt(new Date());
try {
blogCollection.put(blog);
blogCollection.commit();
} catch (GSException e) {
log.error("Error create blog", e);
}
return blog;
}
fetchAll
: Fetching all blog posts.
public List<blog> fetchAll() {
List</blog><blog> blogs = new ArrayList<>(0);
try {
Query</blog><blog> query = blogCollection.query("SELECT *", Blog.class);
RowSet</blog><blog> rowSet = query.fetch();
while (rowSet.hasNext()) {
blogs.add(rowSet.next());
}
} catch (GSException e) {
log.error("Error fetchAll", e);
}
return blogs;
}</blog>
updateVoteUp
: to save the vote-up action. In this method, on fetching row, we use theget(key, forUpdate)
method to lock the row we want to update making other update operations wait until the transaction completes or a timeout occurs.
public void updateVoteUp(String blogId) throws GSException {
blogCollection.setAutoCommit(false);
Blog blog = blogCollection.get(blogId, true);
blog.setVoteUpCount(blog.getVoteUpCount() + 1);
blogCollection.put(blog);
blogCollection.commit();
}
Example of vote metrics rows.
VoteMetrics(timestamp=Sun Jan 21 03:48:10 GMT 2024, blogId=bl_0EWG3KRE8H95G, userId=us_0EWQES1BVRJAN, voteType=1)
VoteMetrics(timestamp=Sun Jan 21 03:48:03 GMT 2024, blogId=bl_0EWG3KRDCHBEM, userId=us_0EWQER8KKRHDF, voteType=1)
VoteMetrics(timestamp=Sun Jan 21 03:48:02 GMT 2024, blogId=bl_0EWQ49HPKRKWB, userId=us_0EWQER2KVRJCJ, voteType=1)
VoteMetrics(timestamp=Sun Jan 21 03:48:00 GMT 2024, blogId=bl_0EWG3KRE8H95G, userId=us_0EWQEQV6FRKKV, voteType=1)
Serving Web Content with Spring MVC
Spring’s web MVC framework is, like many other web MVC frameworks, request-driven, designed around a central Servlet that dispatches requests to controllers and offers other functionality that facilitates the development of web applications. By using Spring MVC Framework we got the following advantages: * The Spring MVC separates each role, where the model object, controller, command object, view resolver, etc. can be fulfilled by a specialized object. * It uses a lightweight servlet container to develop and deploy your application. * It provides a robust configuration for both framework and application classes that includes easy referencing across contexts, such as from web controllers to business objects. * It provides the specific annotations that easily redirect the page.
In this tutorial, we will follow a standard MVC architecture. We will have a controller (VotesController
class), views (votes.html
Thymeleaf template), and a model (a Java map object) for passing data into the view. Every method of the controller is mapped to a URI.
Votes page
In the following example, VotesController
, method votes
handles GET requests for /votes
by returning the name of a View (in this case, votes
), also adding attribute blogs
to Model
via its addAttribute
method.
@Controller
@RequestMapping("/votes")
public class VotesController {
@GetMapping
String votes(Model model) {
List<blog> blogs = blogService.fetchAll();
model.addAttribute("blogs", blogs);
return "votes";
}
@GetMapping("/up/{id}")
public String voteUp(@PathVariable("id") String blogId, Model model, RedirectAttributes redirectAttributes) {
try {
String userId = KeyGenerator.next("us");
voteService.voteUp(blogId, userId);
redirectAttributes.addFlashAttribute("message", "Voting successful!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("message", "Oh no!");
}
return "redirect:/votes";
}
}
</blog>
After creating the controller class, we need to define the template for the views to be generated. We are using Thymeleaf
, a modern server-side Java template engine for both web and standalone environments. The HTML templates written in Thymeleaf still look and work like HTML.
We define an HTML table for displaying a list of blog posts and use th:each
tag attribute to iterate over a collection of blog posts, and th:text
tag for displaying the value.
<tbody>
<tr th:each="blog : ${blogs}">
<th scope="row">[[${blog.title}]]</th>
<td>
<a th:href="@{'/votes/up/' + ${blog.id}}" title="Vote Up" class="btn btn-outline-success"
role="button">
<i class="fa fa-thumbs-up"></i>
<span type="text" th:text="${blog.voteUpCount}" class="btn-label"></span>
</a>
</td>
<td>
<a th:href="@{'/votes/down/' + ${blog.id}}" title="Vote Down" class="btn btn-outline-danger"
role="button">
<i class="fa fa-thumbs-down"></i>
<span type="text" th:text="${blog.voteDownCount}" class="btn-label"></span>
</a>
</td>
<td>[[${blog.createdAt}]]</td>
</tr>
</tbody>
Here is a preview of the votes page. Users should be able to click the thumbs-up/down
icon to vote a post up or down.
Dashboard page
For the real-time dashboard, we use Server-Sent Event. With server-sent events, the server can send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.
To open a connection to the server to begin receiving events from it, create a new EventSource object with the URL(/votes/chart-data
) of a script that generates the events.
script
const ctx = document.getElementById('charts');
const voteChart = new Chart(ctx, config);
/* A small decorator for the JavaScript EventSource API that automatically reconnects */
const eventSource = new ReconnectingEventSource("/votes/chart-data");
Here the EventSource listens for incoming message events and updates the chart dataset.
script
eventSource.onmessage = function (event) {
console.log("Received event: " + event.data);
const data = JSON.parse(event.data);
config.data.labels.splice(0, config.data.labels.length, ...data.map(row => row.label));
config.data.datasets[0].data.splice(0, config.data.datasets[0].data.length, ...data.map(row => row.count));
voteChart.update();
};
The server-side script that sends events must respond using the MIME-type text/event-stream. Each notification is sent as a block of text terminated by a pair of newlines.
@GetMapping("/chart-data")
public SseEmitter streamSseMvc(@RequestHeader Map<String, String> headers) {
SseEmitter emitter = new SseEmitter(Duration.ofMinutes(15).toMillis());
ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
sseMvcExecutor.execute(() -> {
try {
for (int i = 0; true; i++) {
SseEventBuilder event = SseEmitter.event()
.data(voteMetricService.getVoteAggregateOverTime())
.id(eventId);
emitter.send(event);
Thread.sleep(Duration.ofSeconds(30).toMillis());
}
} catch (Exception ex) {
emitter.completeWithError(ex);
}
emitter.complete();
});
return emitter;
}
In this project, we sent the event forever until SseEmitter got a timeout. The server will send new data every 30 seconds.
Response from the SSE endpoint:
Running the Project with Docker Compose
To spin up the project we will utilize Docker, a popular container engine. Containers help ensure that the application runs as expected on any machine. Docker gives us access to Docker Compose, a tool we can use to orchestrate multiple containerized applications together.
First, create dockerfile: dev.Dockerfile
. We use a Maven docker image with JDK21
FROM maven:3.9.5-eclipse-temurin-21-alpine
RUN mkdir /app
WORKDIR /app
COPY pom.xml ./
RUN mvn dependency:go-offline
COPY docker-entrypoint-dev.sh ./
COPY src ./src
Next, create a docker entry-point file: docker-entrypoint-dev.sh
. With this script, we want to compile the code every time the code changes
#!/bin/bash
export TERM=xterm
echo "wait 5s"
sleep 5
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" &
while true; do
watch -d -t -g "ls -lR . | sha1sum" && mvn compile
done
Finally, create the docker-compose file: docker-compose-dev.yml
version: '3.3'
services:
app-dev:
container_name: blogvotingapp-dev
build:
context: ./
dockerfile: dev.Dockerfile
volumes:
- ./src:/app/src
- ./.m2:/root/.m2
environment:
- GRIDDB_NOTIFICATION_MEMBER=griddb-dev:10001
- GRIDDB_CLUSTER_NAME=dockerGridDB
- GRIDDB_USER=admin
- GRIDDB_PASSWORD=admin
- spring.thymeleaf.prefix=file:src/main/resources/templates/
command: sh ./docker-entrypoint-dev.sh
ports:
- 8080:8080
- 35729:35729
- 5005:5005
networks:
- griddbvoting-dev-net
depends_on:
- griddb-dev
griddb-dev:
container_name: griddbvoting-dev
build:
context: ./griddbdocker
dockerfile: Dockerfile531
volumes:
- griddbvoting-dev-vol:/var/lib/gridstore
ports:
- 10001:10001
- 20001:20001
networks:
- griddbvoting-dev-net
networks:
griddbvoting-dev-net:
volumes:
griddbvoting-dev-vol:
Let’s build the docker image using the following command:
docker compose -f docker-compose-dev.yml build
After building the docker image, now run it:
docker compose -f docker-compose-dev.yml up up
The website ready at http://localhost:8080
Conclusion
We have learned how to make a voting platform platform using Spring Boot and GridDB as a database.
We also learned how to develop Spring Boot applications using Docker Compose. Dockerizing a Spring Boot application greatly simplifies the deployment process and ensures a consistent environment across various applications. Also encapsulates the application and its dependencies into a Docker image reducing potential consistencies and conflicts significantly.
If you have any questions about the blog, please create a Stack Overflow post here https://stackoverflow.com/questions/ask?tags=griddb .
Make sure that you use the “griddb” tag so our engineers can quickly reply to your questions.
i am interested
i am interested