
Release 2026.2 brings faster writes and a path to millions of records
evitaDB 2026.2 brings fundamental changes to the write side of the database. New granular, persistent data structures dramatically cut the amount of data that has to be copied, relieve the pressure on the garbage collector and — together with storing only the parts of an index that actually changed — make datasets in the millions of records comfortable to work with. Beyond higher write throughput, the release widens the control you have over transaction conflicts, fixes the atomicity of entity-level operations and adds histograms over reference attributes and over range values, aimed at advanced filtering in e-commerce applications.

This time the work reaches deep into the foundations of the database. Before we declare evitaDB ready for general availability, we want exactly those foundations properly settled — and surgery of this kind is far harder to perform on a finished database than it is now.
Performance improvements
So let's have a look at what this release improves.
Granular data structures
A number of our data structures were plain arrays or bitmaps with no internal subdivision. They kept growing as data was written and, although evitaDB segments indexed data heavily for the sake of read speed, hot spots appeared where the code had to work with very large arrays and bitmaps. Software transactional memory requires immutable data structures, so every modification duplicated such an array — even if only temporarily — and that had an adverse effect on the behavior of the Java garbage collector.
The rework also put an end to indexes that existed twice for no good reason. Earlier versions kept the indexes used for sorting entities by attribute separately from the ones used for lookups — each with its own copy of the values and its own data structure. Now, when an attribute is both filterable and sortable, the two roles share a single structure of values and the sort index keeps only what is genuinely its own: the order of the records. Unique indexes got the same treatment. They used to be one large hash map, serialized in full on every change; today they are a B+ tree with the prefix compression described above, so editing a single record rewrites one page instead of hundreds of kilobytes.
None of this is free, of course. A lookup in a unique index used to run in constant time and is now closer to O(log n) — but it is a logarithm with a very high base, so even a collection of a million records fits into three or four hops. That is a precisely bounded cost, and what we bought with it is an unbounded ceiling on writes. We kept the read-side regression in mind throughout and compensated for it with new optimizations in a number of other places.
Granular persistence to disk
The move to tree structures had one more consequence, and for writes it may matter even more than the in-memory structures themselves. Until now an index was a single indivisible block as far as storage was concerned — changing one attribute of one entity meant the entire index was serialized and written out again when the transaction was committed. On a high-cardinality collection that amounted to hundreds of kilobytes of writes for one trivial change.
While we were down in the guts of the storage layer, we tightened its durability as well. The write-ahead log now carries a rolling CRC32 checksum over all preceding records, so corruption is detected down to the individual byte (existing files convert themselves to the new format). And the state of the database engine can no longer move forward before the corresponding record is safely in the log.
Our own version of RoaringBitmap
Granular control over conflict resolution
evitaDB uses snapshot isolation for all transactions — which, put simply, means readers see all the data in the database exactly as it looked at the moment the session was opened (even if that session lasts tens of minutes). Within that session clients see only this consistent starting state and their own changes on top of it; they never see the work of other clients running in parallel, even if those clients have finished it and successfully committed it to the server. When a client tries to commit its own changes, the server checks whether someone else got there first and touched the same "conflict area" at the same time. The heart of the matter lies in how that conflict area is defined, because its width determines how often such conflicts occur. Until now the area was defined identically for all catalogs and entity types across the entire evitaDB instance, and by default it forbade concurrent changes to one and the same entity (two clients could not, for example, modify anything within the same product at once).
New features
On the functional side, this release focused mainly on extending what histograms can do, so that they match the needs of the e-commerce sector better.
Histograms over reference attributes
The basic limitation of histograms was that they could be used on entity attributes only. In practice they often serve as an alternative to faceted filters in cases where the number of distinct values makes an enumeration (a set of checkboxes) impractical and an interval filter (a slider) is the better visualization. At the same time, the client often does not know in advance which histogram to ask for on a given page, because the very existence of the histogram is tied to the kind of records the primary filter returns. In the "Refrigerators" category, interval filters for width / height / depth make perfect sense, whereas in the "Bakery" category they most certainly do not exist. From the client application's point of view it is therefore far more practical to phrase the query like this:
Find me all products in the "Refrigerators" category (and its subcategories). Out of all their parameters that carry the flag (that belong to a group carrying the flag) marking them as intended for filtering, prepare:
a) a faceted filter, if the parameters (their groups) are marked with the "enumerated type" flag b) a histogram, if the parameters (their groups) are marked with the "interval type" flag
Histograms over range values
We extended the reference histogram mechanism just described in one more direction. evitaDB allows numeric attributes to be stored not only as a single concrete value, but also as a from–to range. A typical example is a product whose dimension can be adjusted within a certain interval to suit the customer's requirements — a worktop that can be cut down to a length between 120 and 180 cm, say. One or both bounds of the range can also be left open, so you can express "100 and up" or "at most 50".
The value expression of a histogram on a reference can therefore now point not only at a plain number, but also at such a range attribute. A product is counted into every histogram value that falls inside its interval, both endpoints included. So a product supporting lengths of 120–180 cm will show up in the results for 120, 150 and 180 cm alike. The client application can thus show the user how many products satisfy a particular requested value, not merely how many products share the same lower or upper bound.
The computation reuses the range indexes evitaDB already maintains for filtering. The database walks the sorted boundaries of the individual intervals and keeps track of which records are active at any given point. That removes the need to test every product against every histogram value separately, and the whole computation can be done in a single pass over the indexed data.
An aside: histograms with equalized buckets
A conventional histogram splits the entire value range into buckets of equal width. With e-commerce data that often leads to impractical results, because values are rarely distributed evenly. If product prices range from €5 to €5,000 but most products cost less than €250, nearly all of them end up in the first bucket and the rest of the histogram stays practically empty. The user then has to aim the slider very precisely within the densely populated area, while a large part of its travel affects only a handful of extreme values.
Small things that come in handy
Bug fixes
Atomicity of entity operations
Atomicity means that an operation is either carried out in full, or not at all. That naturally holds for the transaction as a whole, but it is practically useful to treat its constituent parts the same way — in our case, an operation over a single entity. Such an operation consists of several so-called local mutations: change an attribute, change a price, add a reference. When X out of Y of those mutations go through and the next one hits, say, a uniqueness constraint, the database should undo the effects of the preceding ones and only then throw an exception. The client catches it, reacts to it and carries on within the original transaction — it does not have to throw away all the work it has done so far. In previous versions this cleanup could not be entirely relied upon, so discarding the whole transaction was the safer choice. That pushed developers towards smaller transactions and therefore towards more frequent commits, and commits are expensive from the database's point of view — which brings us right back around to low performance on the write side.
This shortcoming has been removed in the current version — and with it a hidden trap that made the cost of such a partial rollback grow quadratically with the number of entities already processed within the same transaction. The size of batch transactions can therefore be increased substantially, which in turn raises the throughput of writing data into the database.
gRPC call timeouts
Let us close the article with the last open item from the previous version — timeouts at the level of the HTTP/2 protocol that manifested themselves in the gRPC interface. We use the embedded Armeria web server for it; it is extremely well optimized and built on reactive principles, and adapting to it meant subordinating everything to asynchronous processing, which took a while to fine-tune. The final stumbling block was the type of pool used for task processing in combination with the ping interval setting. The HTTP/2 protocol itself knows no deadline for answering a ping — treating an unacknowledged ping as a dead connection is a gRPC convention. And Armeria implements that convention in its own way: it has no separate ping period and ping acknowledgement deadline, both are one and the same value. Whoever pings more often therefore shortens the deadline for the answer at the same time — and the connection then drops even at a moment when a request in progress is being transferred over it. Combined with a busy pool, this occasionally surfaced as a CANCELLED status at the gRPC level. We tracked the cause down and fixed it.
