How to federate independent applications with omniledger.io

Andras Gerlits
ITNEXT
Published in
14 min readOct 18, 2023

--

Our goal is to allow seamless state-management between different applications. Since most applications use databases to manage their own state and since most infrastructure rely on an event-log based message-bus to do this currently, it made sense that we should provide a solution that works over the same infrastructure. A seamless solution should enable developers to not think about the complexities of distributed systems and focus on developing their own applications, as if they “lived” in the same space.

In this article, we’ll describe a toy implementation through an example and show how easily we can federate selected database-tables between different databases by reusing programming techniques already familiar to developers. To achieve this, we’ll:

  • first discuss why what we do is a solution to the problem,
  • show how to start an example environment locally,
  • configure the two applications so that it’s ready to federate data,
  • modify the build-file so that the new modules are started,
  • apply the changes in the code-base
  • run our example and see it in action

You will need:

In other words, you can ignore Docker and the parts in this document that refer to it if you would rather provide your own environment that has the services listed above.

Why ACID

The main reason we have so many problems scaling and building distributed systems is that we tend to think of databases as “our here” and networks as “their there”. In reality, they are both means to the same end, so the data they provide has the same correctness requirements. All the tricks, hard thinking and sheer work we (and cloud-providers) put in around data is centred around the goal of reconciling data from the network with data in our database.

This is exactly what we do. We provide the same guarantees for network-based communication as a database does now. We believe that the best way to fix inter-application consistency is by bridging the silos with the same promises as the ones we build on now, so that they become extensions of our familiar environment.

Environment

As mentioned above, the example needs two databases to run and a log-based message-bus in the cluster. In our example, we chose Redpanda for messaging, since it’s both performant and easy to configure for testing, but we’ll be using the client provided by Kafka. From a programmer’s perspective, this change is transparent, as we’re abstracting these concerns away in our solution.

An enterprise environment with two available databases and a message-bus and its concrete instance via docker in our demo

To supply this environment, we provide a docker-compose.yml file, which users can run in the docker container of their choice. NB: we’re not advocating for or against people running their environments in Docker or that they switch to Redpanda from their current environment, we’re saying that this environment is easy to run and fits our purpose of demonstrating our data-platform.

Overview

At its most simple, elements of a distributed application often look like this:

An application App 1, managed by a team of developers Team 1, talking to the message-bus directly

Each team manages their own application, which relies its own database through some kind of driver (a JDBC-client in this example), talks to the message-bus directly via some messaging-client.

When zoomed out to many teams, the picture becomes the following:

Multiple teams connecting their applications through a message-bus

These two illustrations together show a number of databases connected to each other indirectly, through the application and the message-bus.

In this article, we’ll show you how to move towards this setup:

The whole stack with all the modules

On a first glance, this looks like a complication, but from the developer’s perspective this means the following:

Everything removed

The overall change the developer notices is that the messaging-client has been removed and replaced with tables within their own database, which they can query and update the information they shared earlier via messaging.

Now, if we zoom out, the overall diagram can be redrawn to look like this:

In other words, once the changes are applied, developers see the same database-tables and their contents being shared between them, with the same consistency guarantees they get from their databases without them needing to do anything else than what they’ve been doing already.

With this solution in place, developers can progress their operation’s local- and global states together, atomically and forego all the usual complications present in distributed setups. We provide an overview of how this is accomplished in this article.

Technical implementation

To demonstrate how to do this, we’ll be updating the Spring-Boot Petclinic application with federated tables and we’ll be running two instances of it in parallel, with their data being shared.

We’ll be running both Postgres and MySQL instances from the same codebase, as we’re aiming to keep our example compact. Usually, these databases would serve different applications, but we don’t think this takes away from our wider point.

You can check out the original Petclinic application here:

And our project -which has the modifications we’re discussing- here (private repo):

https://github.com/omniledger/petclinic

We’ll need to:

  • Start the environment in Docker
  • Configure our build to run the required modules
  • Configure the properties of the application to
  • Make changes to the codebase

Start the environment via Docker

In a terminal window, execute:

docker-compose up -d

This will automatically pick up the docker-compose.yml file sitting in the root of the application and download and start the required components: Redpanda, Postgres and MySQL.

Build file (pom.xml)

We need three different modules to run for our application to work.

  • client: it provides the transparent communication between the JDBC-driver and the platform
  • platform: maintains the shared ledger clients send requests to- and synchronizers receive instructions from.
  • synchronizer: merges the changes broadcast by the platform into the database instances

We need to add a new repository for our own packages:

  <repositories>
<repository>
<id>local-project-repository</id>
<name>Local directory repository</name>
<url>file://${project.basedir}/lib-repo</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

Three new Spring-Boot starter modules:

    <dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-spring-client-starter</artifactId>
</dependency>
<dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-spring-sync-starter</artifactId>
</dependency>
<dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-spring-platform-starter</artifactId>
</dependency>

And two database-drivers for both MySQL and Postgres, one for the synchronizer, one for the client.

    <dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-client-mysql</artifactId>
</dependency>
<dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-sync-mysql</artifactId>
</dependency>
<dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-client-postgres</artifactId>
</dependency>
<dependency>
<groupId>io.omniledger</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>omni-sync-postgres</artifactId>
</dependency>

These modules are defined separately so that in the intended environment, these services can be scaled and distributed independently of each other.

Properties

We copy application-mysql.properties to application-federated-mysql.properties and add the following lines:

# Kafka bootstrap servers, as supplied by the environment
spring.kafka.bootstrap-servers=localhost:9092

# The unique id of the database instance
o8r.db-id=2

# The unique id of the instance (JVM) running the application
# Each application instance using the same database must have a different value
o8r.instance-id=0

# The dialect implementations to use for the database instance
o8r.client.dialect=io.omniledger.client.sql.mysql.MySQLClientDialect
o8r.sync.dialect=io.omniledger.sync.mysql.MySQLSyncDialect

Copy application-postgres.properties to application-federated-postgres.properties and add the following lines:

# Kafka bootstrap servers, as supplied by the environment
spring.kafka.bootstrap-servers=localhost:9092

# The unique id of the database instance
o8r.db-id=1

# The unique id of the instance (JVM) running the application
# Each application instance using the same database must have a different value
o8r.instance-id=0

# The dialect implementations to use for the database instance
o8r.client.dialect=io.omniledger.client.sql.postgres.PostgresClientDialect
o8r.sync.dialect=io.omniledger.sync.postgres.PostgresSyncDialect

Code-changes

Since the original petclinic application uses an auto-incrementing field as the primary key for all its entities, and since these primary keys will almost certainly clash on a federated table, we need to change the BaseEntity class to use a UUID:

 @Id
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "uuid2")
@JdbcTypeCode(SqlTypes.VARCHAR)
private UUID id;

Once this is done, all that’s left is to mark entities with a @Federated annotation, which informs the software that this table is shared between members of the platform.

@Entity
@Federated(id = 0)
@Table(name = "owners")
public class Owner extends Person {
...
}

The “id” attribute of the annotation must be unique across all the tables in the entire cluster. This is how the platform knows which table records belongs to.

SQL files

Once this is done, all we’re left with are the schema- and initial data-SQL files. Since we don’t yet support consistency constraints across federated tables (the ones on your local schema are not a problem), we need to remove foreign- and unique-keys except for primary keys.

This doesn’t mean that you can’t join, aggregate or use those keys in SQL in the same way you used to do it before, just that in the beta version, we can’t federate these promises exactly as the SQL standard prescribes them, so until we can (this is a planned feature), they will have to be turned off.

We don’t support many-to-many joins over unnamed tables via JPA. Unnameped many-to-many relationships will not be federated, they need to be mapped explicitly through a federated entity with its own primary key.

For the sake of completeness, we’re including both Postgres and MySQL schema files and the initial load-data that is federated on the first start of the application at the end of the article, in the Appendix.

Running the federated application

Once the above changes have been applied, we can start our Spring-boot applications. We’ll be using maven for this, but the example can very easily be adopted to Gradle or other build-tools.

Federating existing data

First, we’ll start the instance working over MySQL. We do this because we supply the updated SQL data file for MySQL only. Its initial start-up will federate the data found there. We’ll also need to specify another property on the first start, that sends a command to the platform to start tracking time. So, when starting our first application, we’ll need to pass the following two properties:

  • o8r.init.migrate-existing=true
  • o8r.start-token-passing=true

Running the applications

We’re now ready to start our applications. First, make sure that both databases and Redpanda (or Kafka) are up and accepting user-connections and that the properties reflect the connection-details for the services.

Once everything is ready to go, we can issue:

mvn spring-boot:run -Dspring-boot.run.profiles=federated-mysql -Dspring-boot.run.arguments="--o8r.start-token-passing=true --o8r.init.migrate-existing=true"

You can use an alternative to Maven, but for this demo, it is useful since it both builds the application and runs it Spring Boot. Once the application started, open a browser window and point it to https://localhost:8080

You should see something like this:

If you now click FIND OWNERS, leave the search field empty and click the button with the title “Find Owner”, it will take you to the list of owners loaded initially. You can edit the owners or the pets here as you normally would.

Now, we need to start the Postgres-based application. This can be accomplished by issuing the following command:

mvn spring-boot:run -Dspring-boot.run.profiles=federated-postgres

Once it started, open http://localhost:8181/ and navigate to the same page. You might need to wait a few seconds for the initial data to be federated, but they should appear shortly. Once you have this setup running, you can freely modify both instances and observe the changes being cascaded between the instances in a fraction of a second.

As shown above, the other instances do not need to be available for the data to be federated successfully between the different instances. Feel free to play around with the application and please don’t forget to submit a bug-report (along with the log-files) at:

https://github.com/omniledger/petclinic/issues

Recap

In this exercise, we upgraded an old, legacy application to work as a collection of perfectly designed microservices in maybe 5 minutes without expecting our users to learn anything new.

We did this by relying on our software to automatically pick up and federate existing data and then maintain changes and resolve conflicting transactions, providing the same kind of consistency promises we rely on in our databases.

Thanks for trying our software, we appreciate you taking the time and hope you find our solution useful. Please feel free to reach out to us at info@omniledger.io should you have any questions or concerns.

Appendix

SQL files

Postgres schema:

CREATE TABLE IF NOT EXISTS vets (
id VARCHAR PRIMARY KEY,
first_name TEXT,
last_name TEXT
);
CREATE INDEX ON vets (last_name);

CREATE TABLE IF NOT EXISTS specialties (
id VARCHAR PRIMARY KEY,
name TEXT
);
CREATE INDEX ON specialties (name);

CREATE TABLE IF NOT EXISTS vet_specialties (
vet_id VARCHAR NOT NULL,
specialty_id VARCHAR NOT NULL,
UNIQUE (vet_id, specialty_id)
);

CREATE TABLE IF NOT EXISTS types (
id VARCHAR PRIMARY KEY,
name TEXT
);
CREATE INDEX ON types (name);

CREATE TABLE IF NOT EXISTS owners (
id VARCHAR PRIMARY KEY,
first_name TEXT,
last_name TEXT,
address TEXT,
city TEXT,
telephone TEXT
);
CREATE INDEX ON owners (last_name);

CREATE TABLE IF NOT EXISTS pets (
id VARCHAR PRIMARY KEY,
name TEXT,
birth_date DATE,
type_id VARCHAR NOT NULL,
owner_id VARCHAR
);
CREATE INDEX ON pets (name);
CREATE INDEX ON pets (owner_id);

CREATE TABLE IF NOT EXISTS visits (
id VARCHAR PRIMARY KEY,
pet_id VARCHAR,
visit_date DATE,
description TEXT
);
CREATE INDEX ON visits (pet_id);

MySQL schema:

CREATE TABLE IF NOT EXISTS vets (
id VARCHAR(36) NOT NULL PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
INDEX(last_name)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS specialties (
id VARCHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(80),
INDEX(name)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS vet_specialties (
vet_id VARCHAR(36) NOT NULL,
specialty_id VARCHAR(36) NOT NULL,
INDEX (vet_id,specialty_id)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS types (
id VARCHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(80),
INDEX(name)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS owners (
id VARCHAR(36) NOT NULL PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
address VARCHAR(255),
city VARCHAR(80),
telephone VARCHAR(20),
INDEX(last_name)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS pets (
id VARCHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(30),
birth_date DATE,
type_id VARCHAR(36) NOT NULL,
owner_id VARCHAR(36),
INDEX(name),
FOREIGN KEY (owner_id) REFERENCES owners(id),
FOREIGN KEY (type_id) REFERENCES types(id)
) engine=InnoDB;

CREATE TABLE IF NOT EXISTS visits (
id VARCHAR(36) NOT NULL PRIMARY KEY,
pet_id VARCHAR(36),
visit_date DATE,
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;

Initial data-file (for MySQL):

INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('3564675f-2589-4ff3-bd34-f3001663aa31', 'James', 'Carter');
INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('f7bf1cca-8c4b-4630-804f-6d73bc7e0c11', 'Helen', 'Leary');
INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('04772a10-daf4-48d3-82ac-1b6848d6e21c', 'Linda', 'Douglas');
INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('88a40e71-e5a2-4fab-925e-28a529428ce9', 'Rafael', 'Ortega');
INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('b488ded8-5866-4635-92aa-c0bffce8b5b3', 'Henry', 'Stevens');
INSERT IGNORE INTO vets (id, first_name, last_name) VALUES ('0fe88114-f51b-4045-9eff-600e019b8783', 'Sharon', 'Jenkins');

INSERT IGNORE INTO specialties (id, name) VALUES ('07df1156-357d-4be5-ac30-c8b92b21c761', 'radiology');
INSERT IGNORE INTO specialties (id, name) VALUES ('7953fae5-4142-46cd-828f-9f382d4d2fd9', 'surgery');
INSERT IGNORE INTO specialties (id, name) VALUES ('f149ae9b-87e6-4931-b836-429ecf4c4b94', 'dentistry');

INSERT IGNORE INTO vet_specialties (vet_id, specialty_id) VALUES ('f7bf1cca-8c4b-4630-804f-6d73bc7e0c11', '07df1156-357d-4be5-ac30-c8b92b21c761');
INSERT IGNORE INTO vet_specialties (vet_id, specialty_id) VALUES ('04772a10-daf4-48d3-82ac-1b6848d6e21c', '7953fae5-4142-46cd-828f-9f382d4d2fd9');
INSERT IGNORE INTO vet_specialties (vet_id, specialty_id) VALUES ('04772a10-daf4-48d3-82ac-1b6848d6e21c', 'dentistry');
INSERT IGNORE INTO vet_specialties (vet_id, specialty_id) VALUES ('88a40e71-e5a2-4fab-925e-28a529428ce9', '7953fae5-4142-46cd-828f-9f382d4d2fd9');
INSERT IGNORE INTO vet_specialties (vet_id, specialty_id) VALUES ('b488ded8-5866-4635-92aa-c0bffce8b5b3', '07df1156-357d-4be5-ac30-c8b92b21c761');

INSERT IGNORE INTO types (id, name) VALUES ('8dcde6fa-5052-4fe4-a567-7a4588e95708', 'cat');
INSERT IGNORE INTO types (id, name) VALUES ('72150a9e-d24e-47bc-b14a-d9e96b5f3f40', 'dog');
INSERT IGNORE INTO types (id, name) VALUES ('377508cf-569e-46a8-8968-0720a7840e34', 'lizard');
INSERT IGNORE INTO types (id, name) VALUES ('7d340f0e-0f91-48e6-8920-a2e1d8f97319', 'snake');
INSERT IGNORE INTO types (id, name) VALUES ('3f1a7262-a7d9-45dc-9953-748473bf9eb7', 'bird');
INSERT IGNORE INTO types (id, name) VALUES ('38d2038d-84f2-43b6-9a88-74284e7a9cde', 'hamster');

INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('0a97d9c5-fb4b-40b1-b317-4764e165287b', 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('dd1f6bbb-fce9-45f4-bc6e-f2a59eecc0a0', 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('7f000fba-a0b7-486c-8f8b-4dd9f41e2d60', 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('28046a6f-a43a-419a-a7d0-082f6a11d82b', 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('a9cd7119-3549-4390-8061-030d006660bd', 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('ded5ef48-4b2c-45dc-aaeb-f06da0c5ca30', 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('e893d151-7253-4e4e-a1c6-ac1d207ee12c', 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('e0c3b1ce-f42a-40b4-b463-6f74d5cc0ad6', 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('7605001e-faf9-46c1-9912-78d3ec9bd3bf', 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
INSERT IGNORE INTO owners (id, first_name, last_name, address, city, telephone) VALUES ('a885299a-150d-419a-ab10-7007d1ee6ed2', 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');

INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('dfb4fe30-061f-4f3c-bf01-3ed1e606b29e', 'Leo', '2000-09-07', '8dcde6fa-5052-4fe4-a567-7a4588e95708', '0a97d9c5-fb4b-40b1-b317-4764e165287b');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('a32056f4-b540-4c15-9a7f-6a22ace2fe32', 'Basil', '2002-08-06', '38d2038d-84f2-43b6-9a88-74284e7a9cde', 'dd1f6bbb-fce9-45f4-bc6e-f2a59eecc0a0');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('4b584cfa-b412-4f7d-98e2-b5e4a91bc830', 'Rosy', '2001-04-17', '72150a9e-d24e-47bc-b14a-d9e96b5f3f40', '7f000fba-a0b7-486c-8f8b-4dd9f41e2d60');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('01bab90b-bae0-4b0e-aba4-01c996c38adf', 'Jewel', '2000-03-07', '72150a9e-d24e-47bc-b14a-d9e96b5f3f40', '7f000fba-a0b7-486c-8f8b-4dd9f41e2d60');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('d29a3c2c-cfef-4ff6-be4d-fa0d2042782d', 'Iggy', '2000-11-30', '377508cf-569e-46a8-8968-0720a7840e34', '28046a6f-a43a-419a-a7d0-082f6a11d82b');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('d4437023-628f-4dff-a605-30325f6c5cc2', 'George', '2000-01-20', '7d340f0e-0f91-48e6-8920-a2e1d8f97319', 'a9cd7119-3549-4390-8061-030d006660bd');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('f10ec0e7-24d8-407b-9011-b3a3b6874d9c', 'Samantha', '1995-09-04', '8dcde6fa-5052-4fe4-a567-7a4588e95708', 'ded5ef48-4b2c-45dc-aaeb-f06da0c5ca30');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('8ff87f4e-d3c8-4a17-91e2-7111ffe0ab82', 'Max', '1995-09-04', '8dcde6fa-5052-4fe4-a567-7a4588e95708', 'ded5ef48-4b2c-45dc-aaeb-f06da0c5ca30');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('a288513c-db03-4fdb-87be-44b95035fbe5', 'Lucky', '1999-08-06', '3f1a7262-a7d9-45dc-9953-748473bf9eb7', 'e893d151-7253-4e4e-a1c6-ac1d207ee12c');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('825ebb39-b7ab-4dd8-a6a6-2e7cb47cb282', 'Mulligan', '1997-02-24', '72150a9e-d24e-47bc-b14a-d9e96b5f3f40', 'e0c3b1ce-f42a-40b4-b463-6f74d5cc0ad6');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('55cb0e3e-47cf-4bee-84cd-4332335119df', 'Freddy', '2000-03-09', '3f1a7262-a7d9-45dc-9953-748473bf9eb7', '7605001e-faf9-46c1-9912-78d3ec9bd3bf');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('61247023-821a-43a0-a888-f1bd8abb8a6c', 'Lucky', '2000-06-24', '72150a9e-d24e-47bc-b14a-d9e96b5f3f40', 'a885299a-150d-419a-ab10-7007d1ee6ed2');
INSERT IGNORE INTO pets (id, name, birth_date, type_id, owner_id) VALUES ('04fbaff6-5339-4485-81e3-1723f0965765', 'Sly', '2002-06-08', '8dcde6fa-5052-4fe4-a567-7a4588e95708', 'a885299a-150d-419a-ab10-7007d1ee6ed2');

INSERT IGNORE INTO visits (id, pet_id, visit_date, description) VALUES ('e71f1faf-d93f-40fd-aa0d-d5d3e55cf143', 'f10ec0e7-24d8-407b-9011-b3a3b6874d9c', '2010-03-04', 'rabies shot');
INSERT IGNORE INTO visits (id, pet_id, visit_date, description) VALUES ('15cfb048-fb64-48a4-9b73-21fac805b4a4', '8ff87f4e-d3c8-4a17-91e2-7111ffe0ab82', '2011-03-04', 'rabies shot');
INSERT IGNORE INTO visits (id, pet_id, visit_date, description) VALUES ('4bdd3b32-eb4e-4eb8-b394-ca347e6b7322', '8ff87f4e-d3c8-4a17-91e2-7111ffe0ab82', '2009-06-04', 'neutered');
INSERT IGNORE INTO visits (id, pet_id, visit_date, description) VALUES ('54881190-756a-400d-91ed-f089c9b99c6b', 'f10ec0e7-24d8-407b-9011-b3a3b6874d9c', '2008-09-04', 'spayed');

--

--

Writing about distributed consistency. Also founded a company called omniledger.io that helps others with distributed consistency.