Skip to content

QuickStart Guide

Artem Dmitriev edited this page Jun 26, 2017 · 1 revision

Preface

This is a quick start guide to the Reveno framework, in which we will create an artificial banking system. We will cover everything from defining command objects to executing transactions and checking the results of it. Please note that in the given example we are not going to dive into much details and descriptions about particular parts of Reveno, like journaling, snapshotting and etc. You can get complete overview of all major features at Architectural overview page.

Defining transactional model

In the Reveno there are two ways in which you can define your model - mutable or immutable. In the given example we prefer to show the default way - immutable one. First of all we should define Account entity, which represents a single bank client:

public static class Account {
	public final String name;
	public final long balance;

	public Account add(long amount) {
		return new Account(name, balance + amount);
	}

	public Account(String name, long initialBalance) {
		this.name = name;
		this.balance = initialBalance;
	}
}

Since the Reveno applies CQRS pattern, we need to define a view for the Account domain class, which will be used from query model:

public static class AccountView {
	public final long id;
	public final String name;
	public final long balance;

	public AccountView(long id, String name, long balance) {
		this.id = id;
		this.name = name;
		this.balance = balance;
	}
}

Commands and Transaction actions

In order to perform any transaction, you need to execute a command, which will, according to some internal business logic, dispatch special transaction actions, which will perform actual state mutations. But in the given example we could simplify things a bit. Reveno has a special DSL syntax, which is very helpful for the simple cases, especially when the command handler is doing nothing but dispatching single transaction action. That’s exactly looks like our case. But if you have tough latency and throughput requirements, we still recommend you to use basic API. You can find many examples of it. First, we will define a new engine instance with a file system storage, which is located at some temporal directory:

Reveno reveno = new Engine("/tmp/reveno-sample");

Next step is to define view mapper from transactional to query model:

reveno.domain().viewMapper(Account.class, AccountView.class, (id,e,r) -> new AccountView(id, e.name, e.balance));

That's it. Now lets create our new transaction handler, which adds new account to the system:

DynamicCommand createAccount = reveno.domain()
	.transaction("createAccount", (t, c) -> c.repo().store(t.id(), new Account(t.arg(), 0)))
	.uniqueIdFor(Account.class).command();

Let's quickly describe what's going on here. We have created createAccount command object and its handler, which adds new Account to repository, assigning it new ID which was auto generated for the Account class and is accessible from t.id(). We will also need to define some transaction which will debit/credit the balance of an account (for simpleness, debit will be performed with negative numbers):

DynamicCommand changeBalance = reveno.domain()
	.transaction("changeBalance", (t, c) - > c.repo().store(t.longArg(), c.repo().get(Account.class, t.arg()).add(t.intArg("inc"))))
	.command();

changeBalance command requires two arguments to be passed - account ID and balance delta value. First argument is accessed namelessly by t.longArg(), while the second one with name it was passed by - t.intArg("inc"). Since our model is immutable, we must store the new result of Account.add(..) into the repository.

Transaction execution

First transaction which we will execute is a new account creation. But before that, we must start an engine:

reveno.startup();

long accountId = reveno.executeSync(createAccount, map("name", "John"));

And add 10,000$ to the account, for example:

reveno.executeSync(changeBalance, map("id", accountId, "inc", 10_000));

Now your transactional model has single Account entity with balance set to 10,000$. After that, we can access query model and check that all transactions have made appropriate changes to it:

Assert.assertNotNull(reveno.query().find(AccountView.class, accountId));
Assert.assertEquals(reveno.query().find(AccountView.class, accountId).name, "John");
Assert.assertEquals(reveno.query().find(AccountView.class, accountId).balance, 10_000);

Don't forget to shutdown an engine at the end of an application execution:

reveno.shutdown();

Restoring system state

Since the Reveno is transaction processing framework, it is very important to make sure that all changes can be easily restored in case of any failure or server restart. Let's move all engine initialization logic in separate method init() and all checking logic to method checkState() and then try to restore system state from previous place. The whole example now would look like this:

protected Reveno init(String folder) {
	Reveno reveno = new Engine(folder);
	reveno.domain()
		.transaction("createAccount", (t,c) -> c.repo().store(t.id(), new Account(t.arg(), 0)))
		.uniqueIdFor(Account.class)
		.command();
	reveno.domain()
		.transaction("changeBalance", (t,c) -> c.repo().store(t.longArg(), 
                    c.repo().get(Account.class, t.arg()).add(t.intArg("inc"))))
		.command();
	reveno.domain().viewMapper(Account.class, AccountView.class, 
                    (id, e, r) - > new AccountView(id, e.name, e.balance));
	return reveno;
}

protected void checkState(Reveno reveno, long accountId) {
	Assert.assertNotNull(reveno.query().find(AccountView.class, accountId));
	Assert.assertEquals(reveno.query().find(AccountView.class, accountId).name, "John");
	Assert.assertEquals(reveno.query().find(AccountView.class, accountId).balance, 10_000);
}

public void test() throws Exception {
	Reveno reveno = init("/tmp/reveno-sample");
	reveno.startup();

	long accountId = reveno.executeSync("createAccount", map("name", "John"));
	reveno.executeSync("changeBalance", map("id", accountId, "inc", 10_000));

	checkState(reveno, accountId);
	reveno.shutdown();

	reveno = init("/tmp/reveno-sample");
	reveno.startup();

	// we perform no operations, just checking that previous state is restored
	checkState(reveno, accountId);

	reveno.shutdown();
}

All checks are passed after second startup, which means that everything was persisted to storage, and restored after system restart.

Keep growing

In the given example we declared simple transactional model, and views for it, which are part of query model. After that, we just started to performing our transactions on it from scratch. But for now we just managed to see only the tip of an iceberg. Although the Reveno is by default configured with modest values, you still have a numbers of options to make sure it’s best exactly for your case.ou are welcome to continue learning on our Documentation page. Also, you can find other full examples here.