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.
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.
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
If you want to learn more about HTMX + Spring Boot check out my series Web development without the JavaScript headache with Spring + HTMX.
My side business PhotoQuest is also built with HTMX + JTE