Creating a JPA-style CrudRepository using Microstream

Creating a JPA-style CrudRepository using Microstream

Example GitHub Repository

What is Microstream?

MicroStream is a Java native persistence layer that can store a Java Object Graph in a binary storage format, persisting it across application restarts.

With MicroStream your data lives in your RAM giving you blazingly fast access times.

microstream-perfomance.png

Why do we want MicroStream

I wanted to try out using MicroStream after using JPA for most of my developer career as I was looking to simplify my data storage, after hearing about it at JCON 2023.

I'm very interested in making my application as simple as possible to ease development and get to production faster.

They market MicroStream heavily for microservices, but I think the best use case is a fat modulith running on a VPS at Hetzner or similiar.

You can get a 32 Core,64 Thread dedicated Server with 1TB of RAM for 530€ a month at Hetzner, or a 128 Core, 256 Thread dedicated Server with 2TB of RAM for 2400€ a month at hostcircle.nl. Tell me the workload that doesn't fit on that machine.

Migrating to JPA

MicroStream has support for Spring Boot. As we want to store a Java Object Graph we need to define a Root Node for the Graph.

microstream_object_graph.png

We can do that with the @Storage annotation:

@Storage
class Root {
    @Autowired
    @Transient
    private lateinit var storageManager: StorageManager
    //...
}

We autowire the StorageManager that is responsible for interacting with MicroStream.

We want to have a primary key for every Object in the DataStore to emulate the Database behavior we are used to.

Instead of a Database Table PERSON we create a mutableMap with a Long Key and a Person Value.

We also create an AtomicLong as an Index and create a function that increases the index and stores it.

@Storage
class Root {
    //...
    val personMap = mutableMapOf<Long, Person>()

    private val index: AtomicLong = AtomicLong(0)

    fun newIndex(): Long {
        val newIndex = index.incrementAndGet()
        storageManager.store(index)
        return newIndex
    }
}

We create a generic store function to store our map in our Root Object

class Root{
    //...
    fun <T> store(map: MutableMap<Long, T>) {
        storageManager.store(map)
    }
}

To emulate the CrudRepository we know from JPA, I created a generic CrudRepository which we can implement in our Domain Repositories. It expects a TypeArgument which implements the Entity interface which has an id parameter.

It exposes methods like getAll, getByIdOrNull, save, and saveAll.

open class CrudRepository<T : Entity>(
    open val root: Root,
    private val mutableMap: MutableMap<Long, T>,
) {
    private val lock: ReentrantReadWriteLock = ReentrantReadWriteLock()
    fun getAll(): List<T> {
        return readAction {
            mutableMap.values.toList()
        }
    }

    fun getByIdOrNull(id: Long): T? {
        return readAction{
            mutableMap[id]
        }
    }

    fun save(obj: T): T {
        return writeAction{
            (obj.id ?: root.newIndex()).let { id ->
                obj.id = id
                mutableMap[id] = obj
                store()
            }
            obj
        }

    }

    fun saveAll(objList: List<T>): List<T> {
        return writeAction{
            val objMap = objList.map { obj ->
                (obj.id ?: root.newIndex()).let { id ->
                    obj.id = id
                    return@map Pair(id,obj)
                }
            }.toMap()
            mutableMap.putAll(objMap)
            store()
            objMap.values.toList()
        }
    }
    private fun store(){
        root.store(mutableMap)
    }

    open fun <T> readAction(supplier: Supplier<T>): T {
        lock.readLock().lock()
        return try {
            supplier.get()
        } finally {
            lock.readLock().unlock()
        }
    }

    open fun <T> writeAction(supplier: Supplier<T>): T {
        lock.writeLock().lock()
        return try {
            supplier.get()
        } finally {
            lock.writeLock().unlock()
        }
    }
}

We can now implement a JPA style PersonRepository extending our CrudRepository.

In this example we create a findByName function that filters through all map values by name.

@Repository
class PersonRepository(
    override val root: Root
) : CrudRepository<Person>(root, root.personMap){
    fun findByName(name: String): List<Person> {
        return readAction {
            root.personMap.values.filter {
                it.name == name
            }
        }
    }
}

Example

You can find the example in the GitHub Repository