Skip to content

How To Add Compression to Caching in Spring Boot

Free your cache

RAM is one of the most expensive resources offered by cloud providers. So, storing all your cached data in in-memory caches comes with a price. This is why it is essential to implement techniques aimed at not wasting it.

Plus, this becomes practically unavoidable when your Spring Boot application and cache server coexist in the same machine, sharing the underlying resources. In fact, the less RAM the cache steals from the application, the better. Also, serialized Java objects are known to take up a lot of space. So, by caching them, your RAM may run out of space very easily.

This is where compression comes into play!

Let’s see how to add compression to your caching system in Spring Boot in both Kotlin and Java.

Compression and Caching

Normally, when dealing with caching in Spring Boot, data is serialized and then stored in the cache. When needed, data is searched, deserialized, and finally returned to the application in its original format, which is represented in this case by Java/Kotlin objects.

Caching flow graph
Caching flow graph

Adding a compression layer means compressing data after serializing it and decompressing it before deserializing it, modifying the normal flow as follows:

Compressed data caching flow graph
Compressed data caching flow graph

Compressing data reduces the size of your cache and gives you two options:

  1. Reducing the RAM required by the cache server, saving you money.
  2. Keeping the cache the same size but allowing you to save much more data.

Both options are great, but compression also introduces overhead. In particular, compression and decompression come with a cost in terms of time, and this may significantly reduce the performance benefit of caching. This represents a trade-off between RAM and CPU, and it is up to you to find out whether this approach fits your particular case or not.

Implementing the Compression Logic

Keep in mind that the approach presented above can be adopted with any cache provider supported by Spring Boot. Let’s see how to implement it while using a Redis cache. This can easily be achieved by registering a custom-defined class inheriting from JdkSerializationRedisSerializer as the default Redis serializer.

First, you need to define a valid Redis serializer implementing the compression and decompression logic as explained in the graph above. You are going to see how to use GZIP, which is natively implemented in Java, but other compression approaches are possible. Plus, the Commons IO library will be used to keep the decompression logic simple.

If you are a Gradle user, add this dependency to your project’s build file:

compile "commons-io:commons-io:2.9.0"

Otherwise, if you are a Maven user, add the following dependency to your project’s build POM:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.9.0</version>
</dependency>

Now, you have everything required to define a custom Redis serializer.

Java

class RedisCacheGZIPSerializer extends JdkSerializationRedisSerializer {
    @Override
    public Object deserialize(
            byte[] bytes
    ) {
        return super.deserialize(decompress(bytes));
    }

    @Override
    public byte[] serialize(
            Object o
    ) {
        return compress(super.serialize(o));
    }

    private byte[] compress(
            byte[] data
    ) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        
        // compressing the input data using GZIP
        try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
            gzipOutputStream.write(data);
        } catch (IOException e) {
            throw new SerializationException(e.getMessage());
        }

        return byteArrayOutputStream.toByteArray();
    }

    private byte[] decompress(
            byte[] data
    ) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            // decompressing the input data using GZIP
            IOUtils.copy(new GZIPInputStream(new ByteArrayInputStream(data)), out);
        } catch (IOException e) {
            throw new SerializationException(e.getMessage());
        }

        return out.toByteArray();
    }
}

Kotlin

class RedisCacheGZIPSerializer : JdkSerializationRedisSerializer() {
    override fun deserialize(
        bytes: ByteArray?
    ) : Any {
        return super.deserialize(decompress(bytes))
    }

    override fun serialize(
        o: Any?
    ): ByteArray {
        return compress(super.serialize(o))
    }

    private fun compress(
        data: ByteArray
    ) : ByteArray {
        val byteArrayOutputStream = ByteArrayOutputStream()

        try {
            // compressing the input data using GZIP
            GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> gzipOutputStream.write(data) }
        } catch (e: IOException) {
            throw SerializationException(Constants.COMPRESSION_ERROR_MESSAGE, e)
        }

        return byteArrayOutputStream.toByteArray()
    }

    private fun decompress(
        data: ByteArray?
    ) : ByteArray {
        val out = ByteArrayOutputStream()

        try {
            // decompressing the input data using GZIP
            IOUtils.copy(GZIPInputStream(ByteArrayInputStream(data)), out)
        } catch (e: IOException) {
            throw SerializationException(Constants.DECOMPRESSION_ERROR_MESSAGE, e)
        }

        return out.toByteArray()
    }
}

Second, you need to declare the just-defined class as the default Redis value serializer. This can be done by registering a RedisCacheConfiguration primary bean in a custom @Configuration annotated class implementing CachingConfigurerSupport, as follows:

Java

@Configuration
class CacheConfig extends CachingConfigurerSupport {
    @Bean
    @Primary
    public RedisCacheConfiguration defaultCacheConfig() {
        RedisCacheGzipSerializer serializerGzip = new RedisCacheGzipSerializer();

        return RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeValuesWith(SerializationPair.fromSerializer(serializerGzip));
    }
}

Kotlin

@Configuration
class CacheConfig : CachingConfigurerSupport() {
    @Bean
    @Primary
    fun defaultCacheConfig(
    ) : RedisCacheConfiguration? {
      // registering the custom Redis serializer
        val serializerGzip = RedisCacheGzipSerializer()

        return RedisCacheConfiguration
            .defaultCacheConfig()
            .serializeValuesWith(SerializationPair.fromSerializer(serializerGzip))
    }
}

Et voilà! Your Redis cache will now be freer than ever!

Conclusion

Dealing with in-memory caches might significantly increase the cost of your architecture. This is exactly why you should adopt approaches aimed at reducing the space of your cached data. As we have just seen, adding compression logic to a caching system in your Spring Boot application is not complex, and it allows you to save space and money as a consequence.

Thanks for reading! I hope that you found this article helpful.

nv-author-image

Antonello Zanini

I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.View Author posts

Want technical content like this in your blog?