In a previous blog post, I showed how to use Hibernate Envers for auditing entity objects. This approach works great if you are using Hibernate in your application. The problem is, what if you are not using Hibernate? What if you are using a different database technology like MongoDB? In all these cases, you can't use Envers, because it is so tightly coupled with Hibernate. Fortunately, there is an alternative solution called JaVers that can be used in any Java application, regardless of the database technology or ORM framework you are using.
JaVers is an open-source Java library for tracking changes. It provides an object diff engine that compares object graphs and returns detailed atomic changes. The library provides different repository implementations to store these changes into various data stores, not just relational databases. JaVers also brings a powerful query language (JQL) to retrieve historical data, making it easy to track changes over time.
JaVers Diff Engine ¶
The core of JaVers is the diff engine, which is responsible for comparing two objects and returning the differences. The diff engine can handle complex object graphs, including collections and nested objects. It provides annotations to customize the diff behavior, such as ignoring certain fields.
The diff engine can be added to a Maven managed project by adding the following dependency:
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
<version>7.8.0</version>
</dependency>
Here a simple example of the diff engine in action:
The example code first defines three records, User
and Todo
are denoted as entities with the @Id
annotation, while Address
is a value object, it has no unique identity.
JaVers differentiates between entity (has a unique identity), value object (complex objects without identity), and value (single value, e.g. String, BigDecimal, etc.).
To denote an object as entity, you add an @Id
annotation to a field that uniquely identifies the object. JaVers not only recognizes its own @Id
annotation, but also annotations from other libraries like JPA (@jakarta.persistence.Id) and Morphia (@org.mongodb.morphia.annotations.Id). This allows you to use JaVers in applications that already use these libraries without having to change your domain model.
record User(@Id String id, String name, int age, List<String> roles, Address address,
Todo todo) {
}
record Address(String street, String city) {
}
record Todo(@Id String id, String title, boolean completed) {
}
The application then instantiates a Javers
object using the JaversBuilder
.
This is the main entry point to the JaVers diff engine.
Javers javers = JaversBuilder.javers().build();
In the first example, the code compares two value objects. Value objects are compared field by field. Address
only
contains single value fields, so the only change that can occur is a value change represented by the
ValueChange
instance.
Address address = new Address("123 Main St", "Anytown");
Address newAddress = new Address("1234 Main St", "Anytown");
Diff diff = javers.compare(address, newAddress);
for (Change change : diff.getChanges()) {
ValueChange valueChange = (ValueChange) change;
System.out.println("Property '" + valueChange.getPropertyName() + "' changed from '"
+ valueChange.getLeft() + "' to '" + valueChange.getRight() + "'");
}
To compare two objects, you call the compare
method on the Javers
instance, passing in the two objects you want to compare.
The result is a Diff
object
that contains all the changes between the two objects. The getChanges
returns a list of Change
objects.
Change
is an abstract class
that sits on top of the type hierarchy for all change types in JaVers.
In the example above, we know the change can only be a ValueChange
,
so we cast it to ValueChange
to access the specific properties of the change. The concrete type ValueChange
represents a change in a value field, and it provides methods to
access the property name and the old and new values. JaVers does not use the words "old" and "new", but rather "left" and "right".
These two directions refer to the two arguments passed to the compare
method, where the first argument is considered the "left"
side and the second argument is the "right" side.
Output of the above code is
Property 'street' changed from '123 Main St' to '1234 Main St'
You can also compare collections of objects with the compareCollections
method. This method returns a Diff
object that contains all the changes between the two collections. The changes are represented as ListChange
objects, which contain a list of ContainerElementChange
objects. These represent the changes to the individual elements in the collection. Note that getChanges
only returns one ListChange
object, because there is only one collection that has changed.
List<String> h1 = List.of("admin", "editor", "viewer", "reporter");
List<String> h2 = List.of("admin", "viewer", "reporter");
diff = javers.compareCollections(h1, h2, String.class);
for (Change change : diff.getChanges()) {
ListChange listChange = (ListChange) change;
for (ContainerElementChange c : listChange.getChanges()) {
switch (c) {
case ElementValueChange evc -> System.out
.println("Value changed: " + evc.getIndex() + " from '" + evc.getLeftValue()
+ "' to '" + evc.getRightValue() + "'");
case ValueAdded va -> System.out
.println("Added: " + va.getValue() + " at index " + va.getIndex());
case ValueRemoved vr -> System.out
.println("Removed: " + vr.getValue() + " at index " + vr.getIndex());
default -> throw new IllegalArgumentException("Unexpected value: " + c);
}
}
}
The compareCollections
method compares the two collections index by index and results in this output:
Value changed: 1 from 'editor' to 'viewer'
Value changed: 2 from 'viewer' to 'reporter'
Removed: reporter at index 3
Next example compares two entities. The name and age of the user have changed.
User user = new User("U1", "Alice", 30, List.of("admin", "editor"), address, null);
User userAfterValueChange = new User("U1", "Alicia", 31, user.roles(), user.address(),
null);
diff = javers.compare(user, userAfterValueChange);
System.out.println("Has Changes: " + diff.hasChanges());
System.out.println("Diff (Value Changes): \n" + diff.prettyPrint());
for (Change change : diff.getChanges()) {
ValueChange vc = (ValueChange) change;
System.out.println(" ValueChange: Property '" + vc.getPropertyName()
+ "' changed from '" + vc.getLeft() + "' to '" + vc.getRight() + "'");
}
hasChanges
returns true if there are any changes between the two objects. The prettyPrint
method returns a human-readable representation of the diff, which includes all the changes. The output of the prettyPrint
method is:
Diff:
* changes on ch.rasc.javersdemo.DiffExample$User/U1 :
- 'age' changed: '30' -> '31'
- 'name' changed: 'Alice' -> 'Alicia'
If you want to access the changes programmatically, get all the changes with the getChanges
method and then process them accordingly. In this example, we only changed single value fields and therefore the diff engine only created ValueChange
objects.
ValueChange: Property 'name' changed from 'Alice' to 'Alicia'
ValueChange: Property 'age' changed from '30' to '31'
Note that in practice it only makes sense to compare entity objects that have the same identity. You could compare entities with different ids, JaVers allows that, but this comparison results in a NewObject
and ObjectRemoved
change, which is not very useful.
Let's look at an example with more change types. This example instantiates a new Todo
object, which is an entity object and sets it as a property in the User
object. The code also changes the address of the user.
newAddress = new Address("456 Oak Ave", "Newville");
Todo todo1 = new Todo("T1", "Buy groceries", false);
User userAfterAddressAndTodoChange = new User("U1", userAfterValueChange.name(),
userAfterValueChange.age(), user.roles(), newAddress, todo1);
diff = javers.compare(userAfterValueChange, userAfterAddressAndTodoChange);
for (Change change : diff.getChanges()) {
switch (change) {
case NewObject noc -> System.out.println(" New Object: " + noc);
case ReferenceChange rc -> System.out
.println(" ReferenceChange: Property '" + rc.getPropertyName()
+ "' changed from " + rc.getLeft() + " to " + rc.getRight());
case ValueChange vc -> System.out
.println(" ValueChange: Property '" + vc.getPropertyName() + "' changed from '"
+ vc.getLeft() + "' to '" + vc.getRight() + "'");
default -> System.out.println(" Other Change: " + change);
}
}
In the output, we see that entity and value object are treated differently. Because Todo
is an entity, we get a NewObject
change and we get a ReferenceChange
for the property user.todo
. If you compare this to the value object Address
, we only get two ValueChange
objects for the properties street
and city
. Because an Address
object does not have an identity, JaVers cannot determine if this is a new object or an existing object that has changed. It only knows that the properties of the Address
object have changed.
New Object: NewObject{ new object: ch.rasc.javersdemo.DiffExample$Todo/T1 }
ValueChange: Property 'id' changed from 'null' to 'T1'
ValueChange: Property 'title' changed from 'null' to 'Buy groceries'
ReferenceChange: Property 'todo' changed from null to ...DiffExample$Todo/T1
ValueChange: Property 'street' changed from '123 Main St' to '456 Oak Ave'
ValueChange: Property 'city' changed from 'Anytown' to 'Newville'
Note that JaVers by default also creates ValueChange
objects for the initial instantiation of an entity.
In this example, two ValueChange
objects are created for the properties id
and title
of the Todo
object.
These initial changes are not always useful and can lead to a lot of noise if you instantiate many entity objects with a lot of fields.
Therefore, JaVers provides a way to disable these initial changes when you create the Javers
instance.
Javers javers = JaversBuilder.javers().withInitialChanges(false).build();
Annotations ¶
In the examples above, we used the @Id
annotation to denote an entity. Additionally, JaVers provides a set of annotations to customize the diff behavior:
@DiffIgnore
Can be either used on a field, getter method or type. If used on a field or method, it tells JaVers to ignore this field when comparing objects. If used on a type, it tells JaVers to ignore all fields of this type. In a JPA application, you can also use the @jakarta.persistence.Transient
annotation to achieve the same effect.
@DiffInclude
Can be used on a field or getter method to tell JaVers to include this field in the diff. All other properties in this class and all properties in its subclasses will be ignored by JaVers. This annotation could be useful if you only want to track a few properties of a class, instead of adding @DiffIgnore
to all other properties. Note that you can't mix @DiffInclude
and @DiffIgnore
in the same class.
@DiffIgnoreProperties
This is a class annotation that allows you to specify a list of properties or methods to be ignored by JaVers. This is useful if you want to ignore multiple properties without annotating each property individually.
@DiffIgnoreProperties({"roles", "address"})
record User(@Id String id, String name, int age, List<String> roles, Address address,
Todo todo) {
}
@TypeName
JaVers internally creates a global ID for entity objects, which by default is a combination of the fully qualified class name and the id of the object (e.g. ch.rasc.javersdemo.DiffExample$User/U1
). The problem is that you can't rename a class or move a class into a different package without breaking any existing audit records. To avoid this problem, you can use the @TypeName
annotation to specify a custom name for the class. This name is then used as the global ID instead of the fully qualified class name.
@TypeName("User")
record User(@Id String id, String name, int age, List<String> roles, Address address,
Todo todo) {
}
@PropertyName
Similar to the @TypeName
annotation, the @PropertyName
annotation allows you to specify a custom name for a property. This is useful if you need to rename a property without breaking existing audit records.
@ShallowReference
If you annotate an entity with this annotation, JaVers only compares the id and ignores all other properties.
This is useful if you only want to track changes to a reference, but not the changes to the referenced object itself.
record User(@Id String id, String name, int age, List<String> roles,
Address address, @ShallowReference Todo todo) {
}
This concludes our tour of the JaVers diff engine. Check out the JaVers documentation for more details.
On top of the core diff engine, JaVers provides a set of libraries that store the changes in various data stores (Repository), and a query language (JQL) to retrieve the changes. In the next section, we take a look at how to integrate JaVers into a Spring Boot application using jOOQ, PostgreSQL and Spring Security.
Setup JaVers ¶
First, include the JaVers dependencies in your pom.xml
file.
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-persistence-sql</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-jpa</artifactId>
<version>7.8.0</version>
</dependency>
The javers-persistence-sql
has a transitive dependency on javers-core
, so you don't need to add it explicitly. This demo application does not use JPA, but we will leverage one utility class from the javers-spring-jpa
.
The next step is to configure JaVers. This application stores the entities in a PostgreSQL database and I wanted to store the audit records in the same database, so I added the javers-persistence-sql
dependency. This is not a requirement, you can store the audit records separately in a different data store. JaVers currently supports these data stores: MongoDB, H2, PostgreSQL, MySQL, MariaDB, Oracle, Microsoft SQL Server and Redis.
Using the same database has the advantage that storing the entity object and storing the audit records can be done in the same transaction. This way you can be sure that the audit records are always in sync with the entity objects.
This is also what the following configuration does. It configures JaVers so that it takes part in the Spring transaction management.
@Configuration
public class JaversConfig {
private final DataSource dataSource;
private final PlatformTransactionManager transactionManager;
JaversConfig(DataSource dataSource, PlatformTransactionManager transactionManager) {
this.dataSource = dataSource;
this.transactionManager = transactionManager;
}
@Bean
Javers javers() {
JaversSqlRepository sqlRepository = SqlRepositoryBuilder.sqlRepository()
.withConnectionProvider(() -> DataSourceUtils.getConnection(this.dataSource))
.withDialect(DialectName.POSTGRES).build();
return TransactionalJpaJaversBuilder.javers().withTxManager(this.transactionManager)
.registerEntities(InventoryItem.class).registerJaversRepository(sqlRepository)
.build();
}
}
When you start the application the first time, JaversSqlRepository
automatically creates the necessary tables in
the database. The current version of JaVers (7.8.0) creates these tables: jv_commit
, jv_commit_property
, jv_global_id
and jv_snapshot
.
Track changes ¶
In this demo application, only one entity will be tracked, the InventoryItem
entity. Note the @Id
annotation on the id field, which tells JaVers that this is an Entity. The @DiffIgnore
annotation on the createdAt
and updatedAt
fields tells JaVers to ignore these fields when comparing objects.
public class InventoryItem {
@Id
private Long id;
@NotBlank
private String name;
private String description;
@NotNull
@Min(0)
private Integer quantity = 0;
@NotNull
@DecimalMin("0.00")
private BigDecimal price = BigDecimal.ZERO;
private String category;
private String sku;
@DiffIgnore
private LocalDateTime createdAt;
@DiffIgnore
private LocalDateTime updatedAt;
The application uses the following AuditService
to track changes to the entity object. The application calls these methods
everytime something in the object changes.
Unlike the compare
method we saw earlier, the commit
method only expects a single argument, the updated entity object.
JaVers internally automatically retrieves the previous state of the object, compares it and stores the changes in the database.
For deleting an entity object, the application calls the commitShallowDelete
method. This method marks the given object as deleted. Unlike commit
, this method is shallow and only affects the given object. It does not delete anything from the audit tables, it only persists a 'terminal snapshot' of the given object. This means that the object is marked as deleted, but the previous states of the object are still stored in the audit tables. This allows you to retrieve old states of an object even after it has been deleted.
@Service
public class AuditService {
private final Javers javers;
public AuditService(Javers javers) {
this.javers = javers;
}
private static String getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null && authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getName())) {
return authentication.getName();
}
return "system";
}
public Commit commitEntity(Object entity) {
return this.javers.commit(getCurrentUser(), entity);
}
public Commit commitEntityDeletion(Object entity) {
return this.javers.commitShallowDelete(getCurrentUser(), entity);
}
}
Because this demo application uses Spring Security, it can easily access the currently logged-in user. The getCurrentUser
method retrieves the username of this user. The username is passed to the commit
call. JaVers stores this information together with the changes in the audit log. This allows us to see who made the change.
In the InventoryService
, the application uses jOOQ to interact with the database. The InventoryService
class provides methods to create, update, delete and retrieve InventoryItem
objects. In this class the AuditService
is injected and used to track changes to the InventoryItem
objects. Each time an InventoryItem
is modified, the AuditService
is called to commit the changes to the audit tables.
@Service
@Transactional
public class InventoryService {
private final DSLContext dsl;
private final AuditService auditService;
public InventoryService(DSLContext dsl, AuditService auditService) {
this.dsl = dsl;
this.auditService = auditService;
}
public List<InventoryItem> getAllItems() {
return this.dsl.selectFrom(INVENTORY_ITEMS).fetch().into(InventoryItem.class);
}
public InventoryItem getItemById(Long id) {
return this.dsl.selectFrom(INVENTORY_ITEMS).where(INVENTORY_ITEMS.ID.eq(id))
.fetchOne().into(InventoryItem.class);
}
public InventoryItem createItem(InventoryItem item) {
InventoryItem savedItem = save(item);
this.auditService.commitEntity(savedItem);
return savedItem;
}
public InventoryItem updateItem(InventoryItem item) {
InventoryItem savedItem = save(item);
this.auditService.commitEntity(savedItem);
return savedItem;
}
public void deleteItem(Long id) {
InventoryItem item = getItemById(id);
if (item != null) {
deleteById(id);
this.auditService.commitEntityDeletion(item);
}
}
public InventoryItem updateQuantity(Long id, Integer newQuantity) {
InventoryItem item = getItemById(id);
if (item == null) {
throw new RuntimeException("Item not found");
}
item.setQuantity(newQuantity);
InventoryItem savedItem = save(item);
this.auditService.commitEntity(savedItem);
return savedItem;
}
Query changes ¶
Just storing the changes is by itself not very useful. You also need a way to query the changes and retrieve the history of an object. JaVers provides a query language called JQL (JaVers Query Language) to retrieve historical data. JQL is a simple, fluent API to query the stored audit records for changes of a given class, object or property.
JaVers provides three views on an object's history: Changes, Shadows and Snapshots.
Changes are the atomic changes that have been made to an object. They represent the differences between two states of an object. These are the changes from the core diff engine we saw earlier.
Snapshots represent the state of an object at a specific commit, including all properties and relationships, represented as a property:value map.
Shadows are historical versions of a domain object restored from a snapshot. These are instances of your domain object that are created from a snapshot.
To access these views, JaVers provides find*
methods on the Javers
instance. Here are some examples:
@GetMapping("/inventory-items/{id}/snapshots")
public String getInventoryItemSnapshots(@PathVariable Long id,
@RequestParam(defaultValue = "10") int limit) {
List<CdoSnapshot> changes = this.javers.findSnapshots(
QueryBuilder.byInstanceId(id, InventoryItem.class).limit(limit).build());
return this.javers.getJsonConverter().toJson(changes);
}
@GetMapping("/inventory-items/{id}/changes")
public String getInventoryItemChanges(@PathVariable Long id) {
var changes = this.javers
.findChanges(QueryBuilder.byInstanceId(id, InventoryItem.class).build());
return this.javers.getJsonConverter().toJson(changes);
}
@GetMapping("/inventory-items/{id}/shadows")
public List<InventoryItem> getInventoryItemShadows(@PathVariable Long id) {
return this.javers
.findShadows(QueryBuilder.byInstanceId(id, InventoryItem.class).build()).stream()
.map(shadow -> (InventoryItem)shadow.get()).toList();
}
This example only scratches the surface of what you can do with JQL. Check out the official documentation for more examples and detailed information.
Demo ¶
Let's see this in action. I created a simple client that allows me to send requests to the Spring Boot application.
The client first logs in with user1
and creates a new InventoryItem
object. Then it logs in with user2
and updates the quantity of the item. Finally, it logs in again with user1
and deletes the item.
You find the complete source code of the client in the GitHub repository.
Now let's see how this looks when calling the three endpoints defined above.
Changes ¶
The changes endpoint returns a list of Change
objects that represents the changes made to the InventoryItem
object.
[
{
"changeType": "ObjectRemoved",
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.815",
"commitDateInstant": "2025-07-21T09:57:15.815144400Z",
"id": 6.00
}
},
{
"changeType": "TerminalValueChange",
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.815",
"commitDateInstant": "2025-07-21T09:57:15.815144400Z",
"id": 6.00
},
"property": "id",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": 59,
"right": null
},
...
{
"changeType": "ValueChange",
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"commitMetadata": {
"author": "user2",
"properties": [],
"commitDate": "2025-07-21T11:57:15.551",
"commitDateInstant": "2025-07-21T09:57:15.551236400Z",
"id": 5.00
},
"property": "quantity",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": 50,
"right": 65
},
{
"changeType": "NewObject",
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.275",
"commitDateInstant": "2025-07-21T09:57:15.275780400Z",
"id": 4.00
}
},
{
"changeType": "InitialValueChange",
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.275",
"commitDateInstant": "2025-07-21T09:57:15.275780400Z",
"id": 4.00
},
"property": "id",
"propertyChangeType": "PROPERTY_VALUE_CHANGED",
"left": null,
"right": 59
}
...
]
Snapshots ¶
A snapshot is a complete state of an object. Because our entity object went through three changes: initial creation, update and deletion, we see three snapshots. They are by default sorted in reverse chronological order. So the first snapshot is the terminal snapshot and represents that the object has been deleted.
The second snapshot is the state of the object after updating the quantity
property, and the third snapshot is the initial state of the object when it was created. The author
tells you who made the change and the commitDate
tells you when the change was made.
[
{
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.815",
"commitDateInstant": "2025-07-21T09:57:15.815144400Z",
"id": 6.00
},
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"state": {},
"changedProperties": [],
"type": "TERMINAL",
"version": 3
},
{
"commitMetadata": {
"author": "user2",
"properties": [],
"commitDate": "2025-07-21T11:57:15.551",
"commitDateInstant": "2025-07-21T09:57:15.551236400Z",
"id": 5.00
},
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"state": {
"quantity": 65,
"price": 899.99,
"name": "Laptop Computer",
"description": "High-performance laptop for business use",
"id": 59,
"category": "Electronics",
"sku": "LAP-001"
},
"changedProperties": [
"quantity"
],
"type": "UPDATE",
"version": 2
},
{
"commitMetadata": {
"author": "user1",
"properties": [],
"commitDate": "2025-07-21T11:57:15.275",
"commitDateInstant": "2025-07-21T09:57:15.275780400Z",
"id": 4.00
},
"globalId": {
"entity": "ch.rasc.javersdemo.entity.InventoryItem",
"cdoId": 59
},
"state": {
"quantity": 50,
"price": 899.99,
"name": "Laptop Computer",
"description": "High-performance laptop for business use",
"id": 59,
"category": "Electronics",
"sku": "LAP-001"
},
"changedProperties": [
"quantity",
"price",
"name",
"description",
"id",
"category",
"sku"
],
"type": "INITIAL",
"version": 1
}
]
Shadows ¶
Because shadows are created from snapshots, we also get three shadows back. You see here that this is a JSON of the InventoryItem
object. Because these are instances of the entity object, we don't get any metadata like who or when the change was made. Shadows are just the historical versions of the entity object and could be useful if you want to revert to a previous state.
[
{
"id": null,
"name": null,
"description": null,
"quantity": 0,
"price": 0,
"category": null,
"sku": null,
"createdAt": "2025-07-21T11:57:16.1135129",
"updatedAt": "2025-07-21T11:57:16.1135129"
},
{
"id": 59,
"name": "Laptop Computer",
"description": "High-performance laptop for business use",
"quantity": 65,
"price": 899.99,
"category": "Electronics",
"sku": "LAP-001",
"createdAt": "2025-07-21T11:57:16.1155138",
"updatedAt": "2025-07-21T11:57:16.1155138"
},
{
"id": 59,
"name": "Laptop Computer",
"description": "High-performance laptop for business use",
"quantity": 50,
"price": 899.99,
"category": "Electronics",
"sku": "LAP-001",
"createdAt": "2025-07-21T11:57:16.1155138",
"updatedAt": "2025-07-21T11:57:16.1155138"
}
]
Conclusion ¶
You have reached the end of this blog post. I showed you some of the core features of JaVers, how to integrate it into a jOOQ/Spring application and how to track changes. JaVers provides a powerful diff engine that can handle complex object graphs and collections, and it allows you to track changes over time and query them with the JQL query language.
The complete source code of the demo application is available on GitHub.