linkedin insight
Redis in a Spring Boot School Management System: A Journey

Redis in a Spring Boot School Management System: A Journey

Software Development
Sep 7, 2024
5-6 min

Share blog

Introduction

I was recently assigned to a school management project for a client in Riyadh, Saudi Arabia, by my company. The project was built using Spring Boot for the backend and Angular for the frontend. As a member of the backend team, I was excited to dive into this project and work on building a robust system that could handle the complex requirements of managing a school’s data and operations.

The project journey was engaging, filled with challenges and learning opportunities. However, one thing quickly became clear: the amount of data we were pulling from the database on almost every page was substantial. Information like existing grade years, courses, and academic years was needed everywhere—from the dashboard to student profiles to report cards. Each time a user navigated to a different section of the app, we were hitting the database repeatedly for the same data. It wasn’t long before I realized that this approach was not only inefficient but also putting unnecessary strain on our database.

This is where Redis came into the picture.

Redis: Why It Was the Right Choice

Redis is an in-memory data structure store that’s often used as a cache. It’s incredibly fast because it stores data in memory, meaning retrieval times are minimal compared to querying a database. For our school management system, caching this commonly accessed data made perfect sense. Instead of making recurring queries to the database, we could fetch the necessary information from Redis, speeding up our application and reducing the load on our database.

Implementing Redis in Spring Boot:

Creating the Redis Service Interface

To keep things clean and reusable, I started by creating a Redis service interface that would handle basic operations like saving, retrieving, and deleting data in Redis.

javascript
1public interface RedisService<T extends BaseDto> {
2 String generateKey(T entity);
3 void save(T t, Long expiry);
4 Object get(String key);
5 void delete(String key);
6}

Creating the Redis Service Interface

Next, I implemented this interface in a RedisServiceImpl class. This is where the actual interaction with Redis happens. By extending this class, other services in the application can easily cache and retrieve data without worrying about the underlying Redis operations.

javascript
1@Service
2public class RedisServiceImpl<T extends BaseDto> implements RedisService<T> {
3
4 protected final RedisTemplate<String, Object> redisTemplate;
5 protected final ObjectMapper objectMapper;
6 public RedisServiceImpl(RedisTemplate<String, Object> redisTemplate,
7 ObjectMapper objectMapper) {
8 this.redisTemplate = redisTemplate;
9 this.objectMapper = objectMapper;
10 }
11
12 @Override
13 public void save(T entity, Long expiry) {
14 String key = generateKey(entity);
15 redisTemplate.opsForValue().set(key, entity);
16 if (expiry != null) {
17 redisTemplate.expire(key, expiry, TimeUnit.SECONDS);
18 }
19 }
20
21 @Override
22 public Object get(String key) {
23 return redisTemplate.opsForValue().get(key);
24 }
25
26 @Override
27 public void delete(String key) {
28 redisTemplate.delete(key);
29 }
30}

Specialized Service for Grade Years and Courses

One of the key areas where Redis caching was particularly beneficial was in managing grade years and their associated courses. For this, I extended the RedisServiceImpl class in a new service, YearsWithCoursesService. This service not only managed the caching of grade years and courses but also provided methods to retrieve and sort them.

javascript
1@Service
2public class YearsWithCoursesService extends RedisServiceImpl<YearDto> {
3 public YearsWithCoursesService(RedisTemplate<String, Object> redisTemplate,
4 ObjectMapper objectMapper) {
5 super(redisTemplate, objectMapper);
6 }
7
8 @Override
9 public String generateKey(YearDto yearDto) {
10 return "year:" + yearDto.getId();
11 }
12
13 public YearDto getYear(Long yearId) {
14 String key = "year:" + yearId;
15 Object value = get(key);
16 if (value == null) {
17 return null;
18 }
19 return objectMapper.convertValue(value, YearDto.class);
20 }
21
22 public List<YearDto> getYears() {
23 List<YearDto> yearDtos = new ArrayList<>();
24 Set<String> keys = redisTemplate.keys("year*");
25
26 if (keys != null) {
27 for (String key : keys) {
28 Object value = redisTemplate.opsForValue().get(key);
29 if (value != null) {
30 YearDto yearDto = objectMapper.convertValue(value, YearDto.class);
31 yearDtos.add(yearDto);
32 }
33 }
34 }
35
36 return yearDtos.stream().sorted(Comparator.comparing(YearDto::getId)).toList();
37 }
38}

Overcoming Roadblocks: Challenges Faced and How I Tackled Them

Running Redis on a Windows Machine

One of the unexpected challenges I faced during the project was that Redis is primarily designed to run on Unix-based systems like Ubuntu. However, I was developing on a Windows machine, which meant I couldn’t just install Redis directly as I would on Linux.

Solution: Using Windows Subsystem for Linux (WSL)

To get Redis up and running on my Windows machine, I decided to use the Windows Subsystem for Linux (WSL). WSL allows you to run a Linux distribution on Windows, which gave me the flexibility to install and manage Redis just as if I were on a Linux machine. If you would like to know more about how to set up Redis on Windows using WSL check out my blog [reference to blog] !

This setup allowed me to develop and test the Redis integration on my Windows machine without any issues. The use of WSL bridged the gap between the Unix-based nature of Redis and the Windows environment I was working in.

Handling Data Inconsistency

One of the challenges I anticipated was ensuring that the cached data in Redis remained consistent with the database. Since Redis doesn’t automatically sync with the database, I needed a strategy to handle updates, particularly when a user added or modified grade years or courses.

To solve this, I employed a cache invalidation strategy. Whenever there was a change in the database, I forced the system to refresh the Redis cache by setting a forceDB flag. Here’s how I implemented this in a YearService class:

javascript
1@Service
2public class YearService {
3 private final YearsWithCoursesService yearsWithCoursesService;
4
5 @Transactional(readOnly = true)
6 public List<YearDto> findYearsWithCourses(Long schoolId, boolean forceDB) {
7
8 List<YearDto> yearDtos = new ArrayList<>();
9
10 if (!forceDB) {
11 yearDtos = yearsWithCoursesService.getYears();
12 }
13
14 if (yearDtos.isEmpty()) {
15 List<Year> years = yearRepository.findAll();
16 List<Course> courses = courseRepository.findByYearIdIn(
17 years.stream().map(Year::getId).toList());
18
19 Map<Long, List<CourseDto>> coursesByYear = courses.stream()
20 .collect(Collectors.groupingBy(
21 course -> course.getYear().getId(),
22 Collectors.mapping(
23 itm -> createCourse(itm, schoolId),
24 Collectors.toList()
25 )
26 ));
27
28 yearDtos = yearMapper.yearsToYearDtos(years);
29 List<YearDto> existingYearDtos = yearsWithCoursesService.getYears();
30 for (YearDto dto : existingYearDtos) {
31 String key = yearsWithCoursesService.generateKey(dto);
32 yearsWithCoursesService.delete(key);
33 }
34 for (YearDto yearDto : yearDtos) {
35 yearDto.setCourses(coursesByYear.get(yearDto.getId()));
36 if (yearDto.getCourses() == null) {
37 yearDto.setCourses(new ArrayList<>());
38 }
39 yearDto.setStudentsCount(yearDto.getCourses().stream()
40 .map(CourseDto::getStudentsCount).reduce(0, Integer::sum));
41 yearsWithCoursesService.save(yearDto, null);
42 }
43 } else {
44 yearDtos = yearsWithCoursesService.getYears();
45
46 }
47
48 return yearDtos;
49 }
50}

In this setup, if the forceDB flag is set to true, the service fetches fresh data from the database, deletes the outdated cache entries, and saves the updated information in Redis. This way, the application always serves the most accurate and up-to-date data to the users.

Final Thoughts

Using Redis in our Spring Boot project significantly improved the performance of our school management system. By caching frequently accessed data like grade years, courses, and academic years we reduced the load on the database and sped up response times for end users. Implementing the Redis service as a reusable component allowed us to easily manage caching throughout the application.

While there were challenges, like ensuring data consistency and handling serialization, the overall experience was positive. Redis proved to be a powerful tool that, when used correctly, can greatly enhance the performance of any data-intensive application. If you’re dealing with similar challenges, I highly recommend considering Redis as a solution. It’s fast, flexible, and relatively easy to integrate into a Spring Boot project.

And that’s it! I hope you enjoyed joining me on this journey through Redis and Spring Boot and found some useful insights and maybe even a bit of inspiration for your own projects. Happy coding!

Blogs

Discover the latest insights and trends in technology with the Omax Tech Blog.

View All Blogs
DynamoDB multi-tenant architecture for secure data isolation.
6-10 min
August 13, 2026

Multi-Tenancy Patterns in DynamoDB: Silo, Pool, and Bridge Models

If you've already made the jump from a relational database to DynamoDB see our guide on moving relational data from SQL to DynamoDB...

Read More
Cursor IDE generating a Figma design draft from a Jira ticket - visualizing AI‑assisted design workflow.
8-10 min
August 10, 2026

We stopped leaving the IDE to design. Here’s our Cursor → Figma flow

Cursor drafts fast, catches gaps early, and still clips fields and breaks layouts. Here's the real pros-and-cons breakdown of our workflow...

Read More
AWS DevOps Agent automating AI-powered on-call incident response
6-8 min
August 07, 2026

AWS DevOps Agent: How AI is Automating On-Call Incident Response

If you've ever been on call during a production outage, you know how stressful it can be. Alerts start firing, dashboards light up, and suddenly you're jumping between monitoring tools...

Read More
Next.js pre-build script for detecting missing images before deployment
6-10 min
August 06, 2026

Catch Missing Images Before Deploy: A Simple Pre-Build Script for Next.js

How Omax Tech added a lightweight image validation gate to Next.js 15 builds on Vercel...

Read More
Storybook MCP + Amazon Bedrock + Strands: Teaching your LLM to build UI from your real design system catalog
10-15 min
August 04, 2026

Teach your LLM your design system: Storybook MCP + Amazon Bedrock + Strands

How to stop models inventing buttons and make them build UI from your real component catalog. Most "AI UI" demos look great until you paste the markup into a real product. The fix is not a smarter prompt...

Read More
Configure Self Hosted GitLab Repository Mirroring
6-8 min
July 30, 2026

Configure Self Hosted GitLab Repository Mirroring

Self hosted GitLab Repository Mirroring is a powerful feature that automatically synchronizes repositories between GitLab and external Git providers...

Read More
Illustration of Event Sourcing concepts for scalable software architecture and distributed systems.
4-8 min
July 20, 2026

Event Sourcing: A Foundation Guide

Event Sourcing is an architectural pattern where every state change is recorded as an immutable event rather than updating a database row in place...

Read More
AWS cloud security best practices with developer coding environment and cloud technology infrastructure
6-10 min
July 15, 2026

AWS Security Best Practices Every Business Should Follow

As more organizations migrate their applications and critical workloads to AWS, securing cloud environments has become a business priority rather than just an IT responsibility...

Read More
Futuristic cloud computing illustration with glowing data and AI-powered server floating in a digital neon environment.
6-10 min
June 22, 2026

AWS Migration Checklist: A Practical Roadmap for Modern Businesses

Migrating businesses to AWS offers many benefits, including cost optimization, improved security, and greater scalability. However, a successful migration requires careful planning and execution. Otherwise, organizations may experience...

Read More

Ready to Work With Us?

Most engagements start with a 20-minute conversation. No pitch, no pressure - just an honest discussion about what you're building and whether we're the right fit.