diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java
index 3b311d66d58146de84e2d7dc9932d7047483c6aa..ffc493477dca7244b785414726b819cb50c3c2e7 100644
--- a/briar-api/src/org/briarproject/api/contact/ContactManager.java
+++ b/briar-api/src/org/briarproject/api/contact/ContactManager.java
@@ -1,6 +1,7 @@
 package org.briarproject.api.contact;
 
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 
@@ -30,10 +31,10 @@ public interface ContactManager {
 	void removeContact(ContactId c) throws DbException;
 
 	interface AddContactHook {
-		void addingContact(Contact c);
+		void addingContact(Transaction txn, Contact c) throws DbException;
 	}
 
 	interface RemoveContactHook {
-		void removingContact(Contact c);
+		void removingContact(Transaction txn, Contact c) throws DbException;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 435d1025b159e561838478506c42fe82240a5a29..d38dc08d686a20c2428404d5593302e675be95dc 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -26,72 +26,85 @@ import java.util.Map;
 /**
  * Encapsulates the database implementation and exposes high-level operations
  * to other components.
- * <p>
+ * <p/>
  * This interface's methods are blocking, but they do not call out into other
  * components except to broadcast {@link org.briarproject.api.event.Event
  * Events}, so they can safely be called while holding locks.
  */
 public interface DatabaseComponent {
 
-	/** Opens the database and returns true if the database already existed. */
+	/**
+	 * Opens the database and returns true if the database already existed.
+	 */
 	boolean open() throws DbException;
 
-	/** Waits for any open transactions to finish and closes the database. */
+	/**
+	 * Waits for any open transactions to finish and closes the database.
+	 */
 	void close() throws DbException, IOException;
 
-	/** Starts a new transaction and returns an object representing it. */
-	Transaction startTransaction() throws DbException;
-
 	/**
-	 * Aborts the given transaction - no changes made during the transaction
-	 * will be applied to the database.
+	 * Starts a new transaction and returns an object representing it.
 	 */
-	void abortTransaction(Transaction txn);
+	Transaction startTransaction() throws DbException;
 
 	/**
-	 * Commits the given transaction - all changes made during the transaction
-	 * will be applied to the database.
+	 * Ends a transaction. If the transaction's
+	 * {@link Transaction#setComplete() commit} flag is set, the
+	 * transaction is committed, otherwise it is aborted.
 	 */
-	void commitTransaction(Transaction txn) throws DbException;
+	void endTransaction(Transaction txn) throws DbException;
 
 	/**
 	 * Stores a contact associated with the given local and remote pseudonyms,
 	 * and returns an ID for the contact.
 	 */
-	ContactId addContact(Author remote, AuthorId local) throws DbException;
+	ContactId addContact(Transaction txn, Author remote, AuthorId local)
+			throws DbException;
 
-	/** Stores a group. */
-	void addGroup(Group g) throws DbException;
+	/**
+	 * Stores a group.
+	 */
+	void addGroup(Transaction txn, Group g) throws DbException;
 
-	/** Stores a local pseudonym. */
-	void addLocalAuthor(LocalAuthor a) throws DbException;
+	/**
+	 * Stores a local pseudonym.
+	 */
+	void addLocalAuthor(Transaction txn, LocalAuthor a) throws DbException;
 
-	/** Stores a local message. */
-	void addLocalMessage(Message m, ClientId c, Metadata meta, boolean shared)
-			throws DbException;
+	/**
+	 * Stores a local message.
+	 */
+	void addLocalMessage(Transaction txn, Message m, ClientId c, Metadata meta,
+			boolean shared) throws DbException;
 
-	/** Stores a transport. */
-	void addTransport(TransportId t, int maxLatency) throws DbException;
+	/**
+	 * Stores a transport.
+	 */
+	void addTransport(Transaction txn, TransportId t, int maxLatency)
+			throws DbException;
 
 	/**
 	 * Stores transport keys for a newly added contact.
 	 */
-	void addTransportKeys(ContactId c, TransportKeys k) throws DbException;
+	void addTransportKeys(Transaction txn, ContactId c, TransportKeys k)
+			throws DbException;
 
 	/**
 	 * Deletes the message with the given ID. The message ID and any other
 	 * associated data are not deleted.
 	 */
-	void deleteMessage(MessageId m) throws DbException;
+	void deleteMessage(Transaction txn, MessageId m) throws DbException;
 
 	/** Deletes any metadata associated with the given message. */
-	void deleteMessageMetadata(MessageId m) throws DbException;
+	void deleteMessageMetadata(Transaction txn, MessageId m) throws DbException;
 
 	/**
 	 * Returns an acknowledgement for the given contact, or null if there are
 	 * no messages to acknowledge.
 	 */
-	Ack generateAck(ContactId c, int maxMessages) throws DbException;
+	Ack generateAck(Transaction txn, ContactId c, int maxMessages)
+			throws DbException;
 
 	/**
 	 * Returns a batch of raw messages for the given contact, with a total
@@ -99,22 +112,23 @@ public interface DatabaseComponent {
 	 * transport with the given maximum latency. Returns null if there are no
 	 * sendable messages that fit in the given length.
 	 */
-	Collection<byte[]> generateBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException;
+	Collection<byte[]> generateBatch(Transaction txn, ContactId c,
+			int maxLength, int maxLatency) throws DbException;
 
 	/**
 	 * Returns an offer for the given contact for transmission over a
 	 * transport with the given maximum latency, or null if there are no
 	 * messages to offer.
 	 */
-	Offer generateOffer(ContactId c, int maxMessages, int maxLatency)
-			throws DbException;
+	Offer generateOffer(Transaction txn, ContactId c, int maxMessages,
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns a request for the given contact, or null if there are no
 	 * messages to request.
 	 */
-	Request generateRequest(ContactId c, int maxMessages) throws DbException;
+	Request generateRequest(Transaction txn, ContactId c, int maxMessages)
+			throws DbException;
 
 	/**
 	 * Returns a batch of raw messages for the given contact, with a total
@@ -123,158 +137,226 @@ public interface DatabaseComponent {
 	 * requested by the contact are returned. Returns null if there are no
 	 * sendable messages that fit in the given length.
 	 */
-	Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException;
+	Collection<byte[]> generateRequestedBatch(Transaction txn, ContactId c,
+			int maxLength, int maxLatency) throws DbException;
 
-	/** Returns the contact with the given ID. */
-	Contact getContact(ContactId c) throws DbException;
+	/**
+	 * Returns the contact with the given ID.
+	 */
+	Contact getContact(Transaction txn, ContactId c) throws DbException;
 
-	/** Returns all contacts. */
-	Collection<Contact> getContacts() throws DbException;
+	/**
+	 * Returns all contacts.
+	 */
+	Collection<Contact> getContacts(Transaction txn) throws DbException;
 
-	/** Returns all contacts associated with the given local pseudonym. */
-	Collection<ContactId> getContacts(AuthorId a) throws DbException;
+	/**
+	 * Returns all contacts associated with the given local pseudonym.
+	 */
+	Collection<ContactId> getContacts(Transaction txn, AuthorId a)
+			throws DbException;
 
-	/** Returns the unique ID for this device. */
-	DeviceId getDeviceId() throws DbException;
+	/**
+	 * Returns the unique ID for this device.
+	 */
+	DeviceId getDeviceId(Transaction txn) throws DbException;
 
-	/** Returns the group with the given ID. */
-	Group getGroup(GroupId g) throws DbException;
+	/**
+	 * Returns the group with the given ID.
+	 */
+	Group getGroup(Transaction txn, GroupId g) throws DbException;
 
-	/** Returns the metadata for the given group. */
-	Metadata getGroupMetadata(GroupId g) throws DbException;
+	/**
+	 * Returns the metadata for the given group.
+	 */
+	Metadata getGroupMetadata(Transaction txn, GroupId g) throws DbException;
 
-	/** Returns all groups belonging to the given client. */
-	Collection<Group> getGroups(ClientId c) throws DbException;
+	/**
+	 * Returns all groups belonging to the given client.
+	 */
+	Collection<Group> getGroups(Transaction txn, ClientId c) throws DbException;
 
-	/** Returns the local pseudonym with the given ID. */
-	LocalAuthor getLocalAuthor(AuthorId a) throws DbException;
+	/**
+	 * Returns the local pseudonym with the given ID.
+	 */
+	LocalAuthor getLocalAuthor(Transaction txn, AuthorId a) throws DbException;
 
-	/** Returns all local pseudonyms. */
-	Collection<LocalAuthor> getLocalAuthors() throws DbException;
+	/**
+	 * Returns all local pseudonyms.
+	 */
+	Collection<LocalAuthor> getLocalAuthors(Transaction txn) throws DbException;
 
 	/**
 	 * Returns the IDs of any messages that need to be validated by the given
 	 * client.
 	 */
-	Collection<MessageId> getMessagesToValidate(ClientId c) throws DbException;
+	Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c)
+			throws DbException;
 
-	/** Returns the message with the given ID, in serialised form. */
-	byte[] getRawMessage(MessageId m) throws DbException;
+	/**
+	 * Returns the message with the given ID, in serialised form.
+	 */
+	byte[] getRawMessage(Transaction txn, MessageId m) throws DbException;
 
-	/** Returns the metadata for all messages in the given group. */
-	Map<MessageId, Metadata> getMessageMetadata(GroupId g)
+	/**
+	 * Returns the metadata for all messages in the given group.
+	 */
+	Map<MessageId, Metadata> getMessageMetadata(Transaction txn, GroupId g)
 			throws DbException;
 
-	/** Returns the metadata for the given message. */
-	Metadata getMessageMetadata(MessageId m) throws DbException;
+	/**
+	 * Returns the metadata for the given message.
+	 */
+	Metadata getMessageMetadata(Transaction txn, MessageId m)
+			throws DbException;
 
 	/**
 	 * Returns the status of all messages in the given group with respect to
 	 * the given contact.
 	 */
-	Collection<MessageStatus> getMessageStatus(ContactId c, GroupId g)
-			throws DbException;
+	Collection<MessageStatus> getMessageStatus(Transaction txn, ContactId c,
+			GroupId g) throws DbException;
 
 	/**
 	 * Returns the status of the given message with respect to the given
 	 * contact.
 	 */
-	MessageStatus getMessageStatus(ContactId c, MessageId m)
+	MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
 			throws DbException;
 
-	/** Returns all settings in the given namespace. */
-	Settings getSettings(String namespace) throws DbException;
+	/**
+	 * Returns all settings in the given namespace.
+	 */
+	Settings getSettings(Transaction txn, String namespace) throws DbException;
 
-	/** Returns all transport keys for the given transport. */
-	Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
-			throws DbException;
+	/**
+	 * Returns all transport keys for the given transport.
+	 */
+	Map<ContactId, TransportKeys> getTransportKeys(Transaction txn,
+			TransportId t) throws DbException;
 
-	/** Returns the maximum latencies in milliseconds of all transports. */
-	Map<TransportId, Integer> getTransportLatencies() throws DbException;
+	/**
+	 * Returns the maximum latencies in milliseconds of all transports.
+	 */
+	Map<TransportId, Integer> getTransportLatencies(Transaction txn)
+			throws DbException;
 
 	/**
 	 * Increments the outgoing stream counter for the given contact and
 	 * transport in the given rotation period .
 	 */
-	void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
-			throws DbException;
+	void incrementStreamCounter(Transaction txn, ContactId c, TransportId t,
+			long rotationPeriod) throws DbException;
 
-	/** Returns true if the given group is visible to the given contact. */
-	boolean isVisibleToContact(ContactId c, GroupId g) throws DbException;
+	/**
+	 * Returns true if the given group is visible to the given contact.
+	 */
+	boolean isVisibleToContact(Transaction txn, ContactId c, GroupId g)
+			throws DbException;
 
 	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
 	 */
-	void mergeGroupMetadata(GroupId g, Metadata meta) throws DbException;
+	void mergeGroupMetadata(Transaction txn, GroupId g, Metadata meta)
+			throws DbException;
 
 	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * message.
 	 */
-	void mergeMessageMetadata(MessageId m, Metadata meta) throws DbException;
+	void mergeMessageMetadata(Transaction txn, MessageId m, Metadata meta)
+			throws DbException;
 
 	/**
 	 * Merges the given settings with the existing settings in the given
 	 * namespace.
 	 */
-	void mergeSettings(Settings s, String namespace) throws DbException;
+	void mergeSettings(Transaction txn, Settings s, String namespace)
+			throws DbException;
 
-	/** Processes an ack from the given contact. */
-	void receiveAck(ContactId c, Ack a) throws DbException;
+	/**
+	 * Processes an ack from the given contact.
+	 */
+	void receiveAck(Transaction txn, ContactId c, Ack a) throws DbException;
 
-	/** Processes a message from the given contact. */
-	void receiveMessage(ContactId c, Message m) throws DbException;
+	/**
+	 * Processes a message from the given contact.
+	 */
+	void receiveMessage(Transaction txn, ContactId c, Message m)
+			throws DbException;
 
-	/** Processes an offer from the given contact. */
-	void receiveOffer(ContactId c, Offer o) throws DbException;
+	/**
+	 * Processes an offer from the given contact.
+	 */
+	void receiveOffer(Transaction txn, ContactId c, Offer o) throws DbException;
 
-	/** Processes a request from the given contact. */
-	void receiveRequest(ContactId c, Request r) throws DbException;
+	/**
+	 * Processes a request from the given contact.
+	 */
+	void receiveRequest(Transaction txn, ContactId c, Request r)
+			throws DbException;
 
-	/** Removes a contact (and all associated state) from the database. */
-	void removeContact(ContactId c) throws DbException;
+	/**
+	 * Removes a contact (and all associated state) from the database.
+	 */
+	void removeContact(Transaction txn, ContactId c) throws DbException;
 
-	/** Removes a group (and all associated state) from the database. */
-	void removeGroup(Group g) throws DbException;
+	/**
+	 * Removes a group (and all associated state) from the database.
+	 */
+	void removeGroup(Transaction txn, Group g) throws DbException;
 
 	/**
 	 * Removes a local pseudonym (and all associated state) from the database.
 	 */
-	void removeLocalAuthor(AuthorId a) throws DbException;
+	void removeLocalAuthor(Transaction txn, AuthorId a) throws DbException;
 
-	/** Removes a transport (and all associated state) from the database. */
-	void removeTransport(TransportId t) throws DbException;
+	/**
+	 * Removes a transport (and all associated state) from the database.
+	 */
+	void removeTransport(Transaction txn, TransportId t) throws DbException;
 
-	/** Sets the status of the given contact. */
-	void setContactStatus(ContactId c, StorageStatus s) throws DbException;
+	/**
+	 * Sets the status of the given contact.
+	 */
+	void setContactStatus(Transaction txn, ContactId c, StorageStatus s)
+			throws DbException;
 
-	/** Sets the status of the given local pseudonym. */
-	void setLocalAuthorStatus(AuthorId a, StorageStatus s)
-		throws DbException;
+	/**
+	 * Sets the status of the given local pseudonym.
+	 */
+	void setLocalAuthorStatus(Transaction txn, AuthorId a, StorageStatus s)
+			throws DbException;
 
-	/** Marks the given message as shared or unshared. */
-	void setMessageShared(Message m, boolean shared) throws DbException;
+	/**
+	 * Marks the given message as shared or unshared.
+	 */
+	void setMessageShared(Transaction txn, Message m, boolean shared)
+			throws DbException;
 
-	/** Marks the given message as valid or invalid. */
-	void setMessageValid(Message m, ClientId c, boolean valid)
+	/**
+	 * Marks the given message as valid or invalid.
+	 */
+	void setMessageValid(Transaction txn, Message m, ClientId c, boolean valid)
 			throws DbException;
 
 	/**
 	 * Sets the reordering window for the given contact and transport in the
 	 * given rotation period.
 	 */
-	void setReorderingWindow(ContactId c, TransportId t, long rotationPeriod,
-			long base, byte[] bitmap) throws DbException;
+	void setReorderingWindow(Transaction txn, ContactId c, TransportId t,
+			long rotationPeriod, long base, byte[] bitmap) throws DbException;
 
-	/** Makes a group visible or invisible to a contact. */
-	void setVisibleToContact(ContactId c, GroupId g, boolean visible)
-			throws DbException;
+	/**
+	 * Makes a group visible or invisible to a contact.
+	 */
+	void setVisibleToContact(Transaction txn, ContactId c, GroupId g,
+			boolean visible) throws DbException;
 
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
 	 */
-	void updateTransportKeys(Map<ContactId, TransportKeys> keys)
-			throws DbException;
+	void updateTransportKeys(Transaction txn,
+			Map<ContactId, TransportKeys> keys) throws DbException;
 }
diff --git a/briar-api/src/org/briarproject/api/db/Transaction.java b/briar-api/src/org/briarproject/api/db/Transaction.java
index 2ea803b05ef888bc1cfe7602ee4b734c937fe79a..46497a039e8dc8c43a99ca2e11165377030b871b 100644
--- a/briar-api/src/org/briarproject/api/db/Transaction.java
+++ b/briar-api/src/org/briarproject/api/db/Transaction.java
@@ -1,15 +1,38 @@
 package org.briarproject.api.db;
 
-/** A wrapper around a database transaction. */
+/**
+ * A wrapper around a database transaction. Transactions are not thread-safe.
+ */
 public class Transaction {
 
 	private final Object txn;
+	private boolean complete = false;
 
 	public Transaction(Object txn) {
 		this.txn = txn;
 	}
 
+	/**
+	 * Returns the database transaction. The type of the returned object
+	 * depends on the database implementation.
+	 */
 	public Object unbox() {
 		return txn;
 	}
+
+	/**
+	 * Returns true if the transaction is ready to be committed.
+	 */
+	public boolean isComplete() {
+		return complete;
+	}
+
+	/**
+	 * Marks the transaction as ready to be committed. This method must not be
+	 * called more than once.
+	 */
+	public void setComplete() {
+		if (complete) throw new IllegalStateException();
+		complete = true;
+	}
 }
diff --git a/briar-api/src/org/briarproject/api/identity/IdentityManager.java b/briar-api/src/org/briarproject/api/identity/IdentityManager.java
index e872d845edbfb7ce3b21397791ff13bf74fa5c02..f2a4db2d906f1a6cec2e8292ce7a7437d94ae158 100644
--- a/briar-api/src/org/briarproject/api/identity/IdentityManager.java
+++ b/briar-api/src/org/briarproject/api/identity/IdentityManager.java
@@ -1,6 +1,7 @@
 package org.briarproject.api.identity;
 
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 
 import java.util.Collection;
 
@@ -25,10 +26,11 @@ public interface IdentityManager {
 	void removeLocalAuthor(AuthorId a) throws DbException;
 
 	interface AddIdentityHook {
-		void addingIdentity(LocalAuthor a);
+		void addingIdentity(Transaction txn, LocalAuthor a) throws DbException;
 	}
 
 	interface RemoveIdentityHook {
-		void removingIdentity(LocalAuthor a);
+		void removingIdentity(Transaction txn, LocalAuthor a)
+				throws DbException;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
index ff1e854c33447f06a4826d2f03fe82a0cdc691ae..690bc54e23f9fa110a7721b8e5f6284fad39752f 100644
--- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java
+++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
@@ -1,6 +1,8 @@
 package org.briarproject.api.sync;
 
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.Transaction;
 
 /**
  * Responsible for managing message validators and passing them messages to
@@ -35,6 +37,7 @@ public interface ValidationManager {
 	void registerValidationHook(ValidationHook hook);
 
 	interface ValidationHook {
-		void validatingMessage(Message m, ClientId c, Metadata meta);
+		void validatingMessage(Transaction txn, Message m, ClientId c,
+				Metadata meta) throws DbException;
 	}
 }
diff --git a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
index b8f9553c84e62362acbfcec5b59f370015769c54..7bdb3bc450357afc1f5b5c215e902a523ac76829 100644
--- a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
+++ b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
@@ -8,6 +8,7 @@ import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchContactException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.ContactAddedEvent;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.EventBus;
@@ -52,19 +53,31 @@ class ContactManagerImpl implements ContactManager, Service,
 	public boolean start() {
 		// Finish adding/removing any partly added/removed contacts
 		try {
-			for (Contact c : db.getContacts()) {
-				if (c.getStatus().equals(ADDING)) {
-					for (AddContactHook hook : addHooks)
-						hook.addingContact(c);
-					db.setContactStatus(c.getId(), ACTIVE);
-					eventBus.broadcast(new ContactAddedEvent(c.getId()));
-				} else if (c.getStatus().equals(REMOVING)) {
-					for (RemoveContactHook hook : removeHooks)
-						hook.removingContact(c);
-					db.removeContact(c.getId());
-					eventBus.broadcast(new ContactRemovedEvent(c.getId()));
+			List<ContactId> added = new ArrayList<ContactId>();
+			List<ContactId> removed = new ArrayList<ContactId>();
+			Transaction txn = db.startTransaction();
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					if (c.getStatus().equals(ADDING)) {
+						for (AddContactHook hook : addHooks)
+							hook.addingContact(txn, c);
+						db.setContactStatus(txn, c.getId(), ACTIVE);
+						added.add(c.getId());
+					} else if (c.getStatus().equals(REMOVING)) {
+						for (RemoveContactHook hook : removeHooks)
+							hook.removingContact(txn, c);
+						db.removeContact(txn, c.getId());
+						removed.add(c.getId());
+					}
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
+			for (ContactId c : added)
+				eventBus.broadcast(new ContactAddedEvent(c));
+			for (ContactId c : removed)
+				eventBus.broadcast(new ContactRemovedEvent(c));
 			return true;
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -90,24 +103,46 @@ class ContactManagerImpl implements ContactManager, Service,
 	@Override
 	public ContactId addContact(Author remote, AuthorId local)
 			throws DbException {
-		ContactId c = db.addContact(remote, local);
-		Contact contact = db.getContact(c);
-		for (AddContactHook hook : addHooks) hook.addingContact(contact);
-		db.setContactStatus(c, ACTIVE);
+		ContactId c;
+		Transaction txn = db.startTransaction();
+		try {
+			c = db.addContact(txn, remote, local);
+			Contact contact = db.getContact(txn, c);
+			for (AddContactHook hook : addHooks)
+				hook.addingContact(txn, contact);
+			db.setContactStatus(txn, c, ACTIVE);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		eventBus.broadcast(new ContactAddedEvent(c));
 		return c;
 	}
 
 	@Override
 	public Contact getContact(ContactId c) throws DbException {
-		Contact contact = db.getContact(c);
+		Contact contact;
+		Transaction txn = db.startTransaction();
+		try {
+			contact = db.getContact(txn, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		if (contact.getStatus().equals(ACTIVE)) return contact;
 		throw new NoSuchContactException();
 	}
 
 	@Override
 	public Collection<Contact> getContacts() throws DbException {
-		Collection<Contact> contacts = db.getContacts();
+		Collection<Contact> contacts;
+		Transaction txn = db.startTransaction();
+		try {
+			contacts = db.getContacts(txn);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		// Filter out any contacts that are being added or removed
 		List<Contact> active = new ArrayList<Contact>(contacts.size());
 		for (Contact c : contacts)
@@ -117,21 +152,30 @@ class ContactManagerImpl implements ContactManager, Service,
 
 	@Override
 	public void removeContact(ContactId c) throws DbException {
-		Contact contact = db.getContact(c);
-		db.setContactStatus(c, REMOVING);
-		for (RemoveContactHook hook : removeHooks)
-			hook.removingContact(contact);
-		db.removeContact(c);
+		Transaction txn = db.startTransaction();
+		try {
+			removeContact(txn, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		eventBus.broadcast(new ContactRemovedEvent(c));
 	}
 
+	private void removeContact(Transaction txn, ContactId c)
+			throws DbException {
+		Contact contact = db.getContact(txn, c);
+		db.setContactStatus(txn, c, REMOVING);
+		for (RemoveContactHook hook : removeHooks)
+			hook.removingContact(txn, contact);
+		db.removeContact(txn, c);
+	}
+
 	@Override
-	public void removingIdentity(LocalAuthor a) {
+	public void removingIdentity(Transaction txn, LocalAuthor a)
+			throws DbException {
 		// Remove any contacts of the local pseudonym that's being removed
-		try {
-			for (ContactId c : db.getContacts(a.getId())) removeContact(c);
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
+		for (ContactId c : db.getContacts(txn, a.getId()))
+			removeContact(txn, c);
 	}
 }
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 9e46dabb99a9cdcf59f8c76b95c6a8aed9695523..8d4c71166571957f33fb02f291dedf4ca47318f1 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -66,6 +66,8 @@ import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
 import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 
+// TODO: Callers should broadcast events after committing transactions
+
 /**
  * An implementation of DatabaseComponent using reentrant read-write locks.
  * Depending on the JVM's lock implementation, this implementation may allow
@@ -123,76 +125,55 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return new Transaction(db.startTransaction());
 	}
 
-	public void abortTransaction(Transaction txn) {
-		db.abortTransaction(txnClass.cast(txn.unbox()));
+	public void endTransaction(Transaction transaction) throws DbException {
+		T txn = txnClass.cast(transaction.unbox());
+		if (transaction.isComplete()) db.commitTransaction(txn);
+		else db.abortTransaction(txn);
 	}
 
-	public void commitTransaction(Transaction txn) throws DbException {
-		db.commitTransaction(txnClass.cast(txn.unbox()));
+	private T unbox(Transaction transaction) {
+		if (transaction.isComplete()) throw new IllegalStateException();
+		return txnClass.cast(transaction.unbox());
 	}
 
-	public ContactId addContact(Author remote, AuthorId local)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, local))
-				throw new NoSuchLocalAuthorException();
-			if (db.containsContact(txn, remote.getId(), local))
-				throw new ContactExistsException();
-			ContactId c = db.addContact(txn, remote, local);
-			db.commitTransaction(txn);
-			return c;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public ContactId addContact(Transaction transaction, Author remote,
+			AuthorId local) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, local))
+			throw new NoSuchLocalAuthorException();
+		if (db.containsContact(txn, remote.getId(), local))
+			throw new ContactExistsException();
+		return db.addContact(txn, remote, local);
 	}
 
-	public void addGroup(Group g) throws DbException {
+	public void addGroup(Transaction transaction, Group g) throws DbException {
 		boolean added = false;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, g.getId())) {
-				db.addGroup(txn, g);
-				added = true;
-			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g.getId())) {
+			db.addGroup(txn, g);
+			added = true;
 		}
 		if (added) eventBus.broadcast(new GroupAddedEvent(g));
 	}
 
-	public void addLocalAuthor(LocalAuthor a) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, a.getId()))
-				db.addLocalAuthor(txn, a);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void addLocalAuthor(Transaction transaction, LocalAuthor a)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a.getId()))
+			db.addLocalAuthor(txn, a);
 	}
 
-	public void addLocalMessage(Message m, ClientId c, Metadata meta,
-			boolean shared) throws DbException {
+	public void addLocalMessage(Transaction transaction, Message m, ClientId c,
+			Metadata meta, boolean shared) throws DbException {
 		boolean added = false;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, m.getGroupId()))
-				throw new NoSuchGroupException();
-			if (!db.containsMessage(txn, m.getId())) {
-				addMessage(txn, m, VALID, shared, null);
-				added = true;
-			}
-			db.mergeMessageMetadata(txn, m.getId(), meta);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, m.getGroupId()))
+			throw new NoSuchGroupException();
+		if (!db.containsMessage(txn, m.getId())) {
+			addMessage(txn, m, VALID, shared, null);
+			added = true;
+		}
+		db.mergeMessageMetadata(txn, m.getId(), meta);
 		if (added) {
 			eventBus.broadcast(new MessageAddedEvent(m, null));
 			eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
@@ -223,538 +204,327 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void addTransport(TransportId t, int maxLatency) throws DbException {
+	public void addTransport(Transaction transaction, TransportId t,
+			int maxLatency) throws DbException {
 		boolean added = false;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsTransport(txn, t)) {
-				db.addTransport(txn, t, maxLatency);
-				added = true;
-			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
+		T txn = unbox(transaction);
+		if (!db.containsTransport(txn, t)) {
+			db.addTransport(txn, t, maxLatency);
+			added = true;
 		}
 		if (added) eventBus.broadcast(new TransportAddedEvent(t, maxLatency));
 	}
 
-	public void addTransportKeys(ContactId c, TransportKeys k)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsTransport(txn, k.getTransportId()))
-				throw new NoSuchTransportException();
-			db.addTransportKeys(txn, c, k);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void addTransportKeys(Transaction transaction, ContactId c,
+			TransportKeys k) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsTransport(txn, k.getTransportId()))
+			throw new NoSuchTransportException();
+		db.addTransportKeys(txn, c, k);
 	}
 
-	public void deleteMessage(MessageId m) throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsMessage(txn, m))
-					throw new NoSuchMessageException();
-				db.deleteMessage(txn, m);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public void deleteMessage(Transaction transaction, MessageId m)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		db.deleteMessage(txn, m);
 	}
 
-	public void deleteMessageMetadata(MessageId m) throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsMessage(txn, m))
-					throw new NoSuchMessageException();
-				db.deleteMessageMetadata(txn, m);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public void deleteMessageMetadata(Transaction transaction, MessageId m)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		db.deleteMessageMetadata(txn, m);
 	}
 
-	public Ack generateAck(ContactId c, int maxMessages) throws DbException {
+	public Ack generateAck(Transaction transaction, ContactId c,
+			int maxMessages) throws DbException {
 		Collection<MessageId> ids;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			ids = db.getMessagesToAck(txn, c, maxMessages);
-			if (!ids.isEmpty()) db.lowerAckFlag(txn, c, ids);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		ids = db.getMessagesToAck(txn, c, maxMessages);
+		if (!ids.isEmpty()) db.lowerAckFlag(txn, c, ids);
 		if (ids.isEmpty()) return null;
 		return new Ack(ids);
 	}
 
-	public Collection<byte[]> generateBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException {
+	public Collection<byte[]> generateBatch(Transaction transaction,
+			ContactId c, int maxLength, int maxLatency) throws DbException {
 		Collection<MessageId> ids;
-		List<byte[]> messages = new ArrayList<byte[]>();
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			ids = db.getMessagesToSend(txn, c, maxLength);
-			for (MessageId m : ids) {
-				messages.add(db.getRawMessage(txn, m));
-				db.updateExpiryTime(txn, c, m, maxLatency);
-			}
-			if (!ids.isEmpty()) db.lowerRequestedFlag(txn, c, ids);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		List<byte[]> messages;
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		ids = db.getMessagesToSend(txn, c, maxLength);
+		messages = new ArrayList<byte[]>(ids.size());
+		for (MessageId m : ids) {
+			messages.add(db.getRawMessage(txn, m));
+			db.updateExpiryTime(txn, c, m, maxLatency);
+		}
+		if (!ids.isEmpty()) db.lowerRequestedFlag(txn, c, ids);
 		if (messages.isEmpty()) return null;
 		if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
 		return Collections.unmodifiableList(messages);
 	}
 
-	public Offer generateOffer(ContactId c, int maxMessages, int maxLatency)
-			throws DbException {
+	public Offer generateOffer(Transaction transaction, ContactId c,
+			int maxMessages, int maxLatency) throws DbException {
 		Collection<MessageId> ids;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			ids = db.getMessagesToOffer(txn, c, maxMessages);
-			for (MessageId m : ids) db.updateExpiryTime(txn, c, m, maxLatency);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		ids = db.getMessagesToOffer(txn, c, maxMessages);
+		for (MessageId m : ids) db.updateExpiryTime(txn, c, m, maxLatency);
 		if (ids.isEmpty()) return null;
 		return new Offer(ids);
 	}
 
-	public Request generateRequest(ContactId c, int maxMessages)
-			throws DbException {
+	public Request generateRequest(Transaction transaction, ContactId c,
+			int maxMessages) throws DbException {
 		Collection<MessageId> ids;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			ids = db.getMessagesToRequest(txn, c, maxMessages);
-			if (!ids.isEmpty()) db.removeOfferedMessages(txn, c, ids);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		ids = db.getMessagesToRequest(txn, c, maxMessages);
+		if (!ids.isEmpty()) db.removeOfferedMessages(txn, c, ids);
 		if (ids.isEmpty()) return null;
 		return new Request(ids);
 	}
 
-	public Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException {
+	public Collection<byte[]> generateRequestedBatch(Transaction transaction,
+			ContactId c, int maxLength, int maxLatency) throws DbException {
 		Collection<MessageId> ids;
-		List<byte[]> messages = new ArrayList<byte[]>();
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			ids = db.getRequestedMessagesToSend(txn, c, maxLength);
-			for (MessageId m : ids) {
-				messages.add(db.getRawMessage(txn, m));
-				db.updateExpiryTime(txn, c, m, maxLatency);
-			}
-			if (!ids.isEmpty()) db.lowerRequestedFlag(txn, c, ids);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		List<byte[]> messages;
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		ids = db.getRequestedMessagesToSend(txn, c, maxLength);
+		messages = new ArrayList<byte[]>(ids.size());
+		for (MessageId m : ids) {
+			messages.add(db.getRawMessage(txn, m));
+			db.updateExpiryTime(txn, c, m, maxLatency);
+		}
+		if (!ids.isEmpty()) db.lowerRequestedFlag(txn, c, ids);
 		if (messages.isEmpty()) return null;
 		if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
 		return Collections.unmodifiableList(messages);
 	}
 
-	public Contact getContact(ContactId c) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			Contact contact = db.getContact(txn, c);
-			db.commitTransaction(txn);
-			return contact;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Contact getContact(Transaction transaction, ContactId c)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		return db.getContact(txn, c);
 	}
 
-	public Collection<Contact> getContacts() throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Collection<Contact> contacts = db.getContacts(txn);
-			db.commitTransaction(txn);
-			return contacts;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Collection<Contact> getContacts(Transaction transaction)
+			throws DbException {
+		T txn = unbox(transaction);
+		return db.getContacts(txn);
 	}
 
-	public Collection<ContactId> getContacts(AuthorId a) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, a))
-				throw new NoSuchLocalAuthorException();
-			Collection<ContactId> contacts = db.getContacts(txn, a);
-			db.commitTransaction(txn);
-			return contacts;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Collection<ContactId> getContacts(Transaction transaction,
+			AuthorId a) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a))
+			throw new NoSuchLocalAuthorException();
+		return db.getContacts(txn, a);
 	}
 
-	public DeviceId getDeviceId() throws DbException {
-		T txn = db.startTransaction();
-		try {
-			DeviceId id = db.getDeviceId(txn);
-			db.commitTransaction(txn);
-			return id;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public DeviceId getDeviceId(Transaction transaction) throws DbException {
+		T txn = unbox(transaction);
+		return db.getDeviceId(txn);
 	}
 
-	public Group getGroup(GroupId g) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			Group group = db.getGroup(txn, g);
-			db.commitTransaction(txn);
-			return group;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
-	}
-
-	public Metadata getGroupMetadata(GroupId g) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			Metadata metadata = db.getGroupMetadata(txn, g);
-			db.commitTransaction(txn);
-			return metadata;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Group getGroup(Transaction transaction, GroupId g)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.getGroup(txn, g);
 	}
 
-	public Collection<Group> getGroups(ClientId c) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Collection<Group> groups = db.getGroups(txn, c);
-			db.commitTransaction(txn);
-			return groups;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Metadata getGroupMetadata(Transaction transaction, GroupId g)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.getGroupMetadata(txn, g);
 	}
 
-	public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, a))
-				throw new NoSuchLocalAuthorException();
-			LocalAuthor localAuthor = db.getLocalAuthor(txn, a);
-			db.commitTransaction(txn);
-			return localAuthor;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Collection<Group> getGroups(Transaction transaction, ClientId c)
+			throws DbException {
+		T txn = unbox(transaction);
+		return db.getGroups(txn, c);
 	}
 
-	public Collection<LocalAuthor> getLocalAuthors() throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Collection<LocalAuthor> authors = db.getLocalAuthors(txn);
-			db.commitTransaction(txn);
-			return authors;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public LocalAuthor getLocalAuthor(Transaction transaction, AuthorId a)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a))
+			throw new NoSuchLocalAuthorException();
+		return db.getLocalAuthor(txn, a);
 	}
 
-	public Collection<MessageId> getMessagesToValidate(ClientId c)
+	public Collection<LocalAuthor> getLocalAuthors(Transaction transaction)
 			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Collection<MessageId> ids = db.getMessagesToValidate(txn, c);
-			db.commitTransaction(txn);
-			return ids;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		return db.getLocalAuthors(txn);
 	}
 
-	public byte[] getRawMessage(MessageId m) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsMessage(txn, m))
-				throw new NoSuchMessageException();
-			byte[] raw = db.getRawMessage(txn, m);
-			db.commitTransaction(txn);
-			return raw;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Collection<MessageId> getMessagesToValidate(Transaction transaction,
+			ClientId c) throws DbException {
+		T txn = unbox(transaction);
+		return db.getMessagesToValidate(txn, c);
 	}
 
-	public Map<MessageId, Metadata> getMessageMetadata(GroupId g)
+	public byte[] getRawMessage(Transaction transaction, MessageId m)
 			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
-			db.commitTransaction(txn);
-			return metadata;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getRawMessage(txn, m);
 	}
 
-	public Metadata getMessageMetadata(MessageId m) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsMessage(txn, m))
-				throw new NoSuchMessageException();
-			Metadata metadata = db.getMessageMetadata(txn, m);
-			db.commitTransaction(txn);
-			return metadata;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
+			GroupId g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.getMessageMetadata(txn, g);
 	}
 
-	public Collection<MessageStatus> getMessageStatus(ContactId c, GroupId g)
+	public Metadata getMessageMetadata(Transaction transaction, MessageId m)
 			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			Collection<MessageStatus> statuses = db.getMessageStatus(txn, c, g);
-			db.commitTransaction(txn);
-			return statuses;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getMessageMetadata(txn, m);
 	}
 
-	public MessageStatus getMessageStatus(ContactId c, MessageId m)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsMessage(txn, m))
-				throw new NoSuchMessageException();
-			MessageStatus status = db.getMessageStatus(txn, c, m);
-			db.commitTransaction(txn);
-			return status;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Collection<MessageStatus> getMessageStatus(Transaction transaction,
+			ContactId c, GroupId g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.getMessageStatus(txn, c, g);
 	}
 
-	public Settings getSettings(String namespace) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Settings s = db.getSettings(txn, namespace);
-			db.commitTransaction(txn);
-			return s;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public MessageStatus getMessageStatus(Transaction transaction, ContactId c,
+			MessageId m) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getMessageStatus(txn, c, m);
 	}
 
-	public Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
+	public Settings getSettings(Transaction transaction, String namespace)
 			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsTransport(txn, t))
-				throw new NoSuchTransportException();
-			Map<ContactId, TransportKeys> keys = db.getTransportKeys(txn, t);
-			db.commitTransaction(txn);
-			return keys;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		return db.getSettings(txn, namespace);
 	}
 
-	public Map<TransportId, Integer> getTransportLatencies()
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Map<TransportId, Integer> latencies = db.getTransportLatencies(txn);
-			db.commitTransaction(txn);
-			return latencies;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Map<ContactId, TransportKeys> getTransportKeys(
+			Transaction transaction, TransportId t) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsTransport(txn, t))
+			throw new NoSuchTransportException();
+		return db.getTransportKeys(txn, t);
 	}
 
-	public void incrementStreamCounter(ContactId c, TransportId t,
-			long rotationPeriod) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsTransport(txn, t))
-				throw new NoSuchTransportException();
-			db.incrementStreamCounter(txn, c, t, rotationPeriod);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public Map<TransportId, Integer> getTransportLatencies(
+			Transaction transaction) throws DbException {
+		T txn = unbox(transaction);
+		return db.getTransportLatencies(txn);
 	}
 
-	public boolean isVisibleToContact(ContactId c, GroupId g)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			boolean visible = db.containsVisibleGroup(txn, c, g);
-			db.commitTransaction(txn);
-			return visible;
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void incrementStreamCounter(Transaction transaction, ContactId c,
+			TransportId t, long rotationPeriod) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsTransport(txn, t))
+			throw new NoSuchTransportException();
+		db.incrementStreamCounter(txn, c, t, rotationPeriod);
 	}
 
-	public void mergeGroupMetadata(GroupId g, Metadata meta)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			db.mergeGroupMetadata(txn, g, meta);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public boolean isVisibleToContact(Transaction transaction, ContactId c,
+			GroupId g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		return db.containsVisibleGroup(txn, c, g);
 	}
 
-	public void mergeMessageMetadata(MessageId m, Metadata meta)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsMessage(txn, m))
-				throw new NoSuchMessageException();
-			db.mergeMessageMetadata(txn, m, meta);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void mergeGroupMetadata(Transaction transaction, GroupId g,
+			Metadata meta) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		db.mergeGroupMetadata(txn, g, meta);
+	}
+
+	public void mergeMessageMetadata(Transaction transaction, MessageId m,
+			Metadata meta) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		db.mergeMessageMetadata(txn, m, meta);
 	}
 
-	public void mergeSettings(Settings s, String namespace) throws DbException {
+	public void mergeSettings(Transaction transaction, Settings s,
+			String namespace) throws DbException {
 		boolean changed = false;
-		T txn = db.startTransaction();
-		try {
-			Settings old = db.getSettings(txn, namespace);
-			Settings merged = new Settings();
-			merged.putAll(old);
-			merged.putAll(s);
-			if (!merged.equals(old)) {
-				db.mergeSettings(txn, s, namespace);
-				changed = true;
-			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
+		T txn = unbox(transaction);
+		Settings old = db.getSettings(txn, namespace);
+		Settings merged = new Settings();
+		merged.putAll(old);
+		merged.putAll(s);
+		if (!merged.equals(old)) {
+			db.mergeSettings(txn, s, namespace);
+			changed = true;
 		}
 		if (changed) eventBus.broadcast(new SettingsUpdatedEvent(namespace));
 	}
 
-	public void receiveAck(ContactId c, Ack a) throws DbException {
+	public void receiveAck(Transaction transaction, ContactId c, Ack a)
+			throws DbException {
 		Collection<MessageId> acked = new ArrayList<MessageId>();
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			for (MessageId m : a.getMessageIds()) {
-				if (db.containsVisibleMessage(txn, c, m)) {
-					db.raiseSeenFlag(txn, c, m);
-					acked.add(m);
-				}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		for (MessageId m : a.getMessageIds()) {
+			if (db.containsVisibleMessage(txn, c, m)) {
+				db.raiseSeenFlag(txn, c, m);
+				acked.add(m);
 			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
 		}
 		eventBus.broadcast(new MessagesAckedEvent(c, acked));
 	}
 
-	public void receiveMessage(ContactId c, Message m) throws DbException {
+	public void receiveMessage(Transaction transaction, ContactId c, Message m)
+			throws DbException {
 		boolean duplicate, visible;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			duplicate = db.containsMessage(txn, m.getId());
-			visible = db.containsVisibleGroup(txn, c, m.getGroupId());
-			if (visible) {
-				if (!duplicate) addMessage(txn, m, UNKNOWN, false, c);
-				db.raiseAckFlag(txn, c, m.getId());
-			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		duplicate = db.containsMessage(txn, m.getId());
+		visible = db.containsVisibleGroup(txn, c, m.getGroupId());
+		if (visible) {
+			if (!duplicate) addMessage(txn, m, UNKNOWN, false, c);
+			db.raiseAckFlag(txn, c, m.getId());
 		}
 		if (visible) {
 			if (!duplicate) eventBus.broadcast(new MessageAddedEvent(m, c));
@@ -762,228 +532,157 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void receiveOffer(ContactId c, Offer o) throws DbException {
+	public void receiveOffer(Transaction transaction, ContactId c, Offer o)
+			throws DbException {
 		boolean ack = false, request = false;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			int count = db.countOfferedMessages(txn, c);
-			for (MessageId m : o.getMessageIds()) {
-				if (db.containsVisibleMessage(txn, c, m)) {
-					db.raiseSeenFlag(txn, c, m);
-					db.raiseAckFlag(txn, c, m);
-					ack = true;
-				} else if (count < MAX_OFFERED_MESSAGES) {
-					db.addOfferedMessage(txn, c, m);
-					request = true;
-					count++;
-				}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		int count = db.countOfferedMessages(txn, c);
+		for (MessageId m : o.getMessageIds()) {
+			if (db.containsVisibleMessage(txn, c, m)) {
+				db.raiseSeenFlag(txn, c, m);
+				db.raiseAckFlag(txn, c, m);
+				ack = true;
+			} else if (count < MAX_OFFERED_MESSAGES) {
+				db.addOfferedMessage(txn, c, m);
+				request = true;
+				count++;
 			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
 		}
 		if (ack) eventBus.broadcast(new MessageToAckEvent(c));
 		if (request) eventBus.broadcast(new MessageToRequestEvent(c));
 	}
 
-	public void receiveRequest(ContactId c, Request r) throws DbException {
+	public void receiveRequest(Transaction transaction, ContactId c, Request r)
+			throws DbException {
 		boolean requested = false;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			for (MessageId m : r.getMessageIds()) {
-				if (db.containsVisibleMessage(txn, c, m)) {
-					db.raiseRequestedFlag(txn, c, m);
-					db.resetExpiryTime(txn, c, m);
-					requested = true;
-				}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		for (MessageId m : r.getMessageIds()) {
+			if (db.containsVisibleMessage(txn, c, m)) {
+				db.raiseRequestedFlag(txn, c, m);
+				db.resetExpiryTime(txn, c, m);
+				requested = true;
 			}
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
 		}
 		if (requested) eventBus.broadcast(new MessageRequestedEvent(c));
 	}
 
-	public void removeContact(ContactId c) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			db.removeContact(txn, c);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void removeContact(Transaction transaction, ContactId c)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		db.removeContact(txn, c);
 	}
 
-	public void removeGroup(Group g) throws DbException {
+	public void removeGroup(Transaction transaction, Group g)
+			throws DbException {
 		Collection<ContactId> affected;
-		T txn = db.startTransaction();
-		try {
-			GroupId id = g.getId();
-			if (!db.containsGroup(txn, id))
-				throw new NoSuchGroupException();
-			affected = db.getVisibility(txn, id);
-			db.removeGroup(txn, id);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		GroupId id = g.getId();
+		if (!db.containsGroup(txn, id))
+			throw new NoSuchGroupException();
+		affected = db.getVisibility(txn, id);
+		db.removeGroup(txn, id);
 		eventBus.broadcast(new GroupRemovedEvent(g));
 		eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
 	}
 
-	public void removeLocalAuthor(AuthorId a) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, a))
-				throw new NoSuchLocalAuthorException();
-			db.removeLocalAuthor(txn, a);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void removeLocalAuthor(Transaction transaction, AuthorId a)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a))
+			throw new NoSuchLocalAuthorException();
+		db.removeLocalAuthor(txn, a);
 	}
 
-	public void removeTransport(TransportId t) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsTransport(txn, t))
-				throw new NoSuchTransportException();
-			db.removeTransport(txn, t);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void removeTransport(Transaction transaction, TransportId t)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsTransport(txn, t))
+			throw new NoSuchTransportException();
+		db.removeTransport(txn, t);
 		eventBus.broadcast(new TransportRemovedEvent(t));
 	}
 
-	public void setContactStatus(ContactId c, StorageStatus s)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			db.setContactStatus(txn, c, s);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void setContactStatus(Transaction transaction, ContactId c,
+			StorageStatus s) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		db.setContactStatus(txn, c, s);
 	}
 
-	public void setLocalAuthorStatus(AuthorId a, StorageStatus s)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsLocalAuthor(txn, a))
-				throw new NoSuchLocalAuthorException();
-			db.setLocalAuthorStatus(txn, a, s);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void setLocalAuthorStatus(Transaction transaction, AuthorId a,
+			StorageStatus s) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a))
+			throw new NoSuchLocalAuthorException();
+		db.setLocalAuthorStatus(txn, a, s);
 	}
 
-	public void setMessageShared(Message m, boolean shared)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsMessage(txn, m.getId()))
-				throw new NoSuchMessageException();
-			db.setMessageShared(txn, m.getId(), shared);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void setMessageShared(Transaction transaction, Message m,
+			boolean shared) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m.getId()))
+			throw new NoSuchMessageException();
+		db.setMessageShared(txn, m.getId(), shared);
 		if (shared) eventBus.broadcast(new MessageSharedEvent(m));
 	}
 
-	public void setMessageValid(Message m, ClientId c, boolean valid)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsMessage(txn, m.getId()))
-				throw new NoSuchMessageException();
-			db.setMessageValid(txn, m.getId(), valid);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void setMessageValid(Transaction transaction, Message m, ClientId c,
+			boolean valid) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m.getId()))
+			throw new NoSuchMessageException();
+		db.setMessageValid(txn, m.getId(), valid);
 		eventBus.broadcast(new MessageValidatedEvent(m, c, false, valid));
 	}
 
-	public void setReorderingWindow(ContactId c, TransportId t,
-			long rotationPeriod, long base, byte[] bitmap) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsTransport(txn, t))
-				throw new NoSuchTransportException();
-			db.setReorderingWindow(txn, c, t, rotationPeriod, base, bitmap);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+	public void setReorderingWindow(Transaction transaction, ContactId c,
+			TransportId t, long rotationPeriod, long base, byte[] bitmap)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsTransport(txn, t))
+			throw new NoSuchTransportException();
+		db.setReorderingWindow(txn, c, t, rotationPeriod, base, bitmap);
 	}
 
-	public void setVisibleToContact(ContactId c, GroupId g, boolean visible)
-			throws DbException {
+	public void setVisibleToContact(Transaction transaction, ContactId c,
+			GroupId g, boolean visible) throws DbException {
 		boolean wasVisible;
-		T txn = db.startTransaction();
-		try {
-			if (!db.containsContact(txn, c))
-				throw new NoSuchContactException();
-			if (!db.containsGroup(txn, g))
-				throw new NoSuchGroupException();
-			wasVisible = db.containsVisibleGroup(txn, c, g);
-			if (visible && !wasVisible) db.addVisibility(txn, c, g);
-			else if (!visible && wasVisible) db.removeVisibility(txn, c, g);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		wasVisible = db.containsVisibleGroup(txn, c, g);
+		if (visible && !wasVisible) db.addVisibility(txn, c, g);
+		else if (!visible && wasVisible) db.removeVisibility(txn, c, g);
 		if (visible != wasVisible) {
 			eventBus.broadcast(new GroupVisibilityUpdatedEvent(
 					Collections.singletonList(c)));
 		}
 	}
 
-	public void updateTransportKeys(Map<ContactId, TransportKeys> keys)
-			throws DbException {
-		T txn = db.startTransaction();
-		try {
-			Map<ContactId, TransportKeys> filtered =
-					new HashMap<ContactId, TransportKeys>();
-			for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
-				ContactId c = e.getKey();
-				TransportKeys k = e.getValue();
-				if (db.containsContact(txn, c)
-						&& db.containsTransport(txn, k.getTransportId())) {
-					filtered.put(c, k);
-				}
+	public void updateTransportKeys(Transaction transaction,
+			Map<ContactId, TransportKeys> keys) throws DbException {
+		Map<ContactId, TransportKeys> filtered =
+				new HashMap<ContactId, TransportKeys>();
+		T txn = unbox(transaction);
+		for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
+			ContactId c = e.getKey();
+			TransportKeys k = e.getValue();
+			if (db.containsContact(txn, c)
+					&& db.containsTransport(txn, k.getTransportId())) {
+				filtered.put(c, k);
 			}
-			db.updateTransportKeys(txn, filtered);
-			db.commitTransaction(txn);
-		} catch (DbException e) {
-			db.abortTransaction(txn);
-			throw e;
 		}
+		db.updateTransportKeys(txn, filtered);
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 6820afb47e92cac1958dfbf41dc00bae31e18459..9da89c67191477761cae2178c48ed4afe4fb719c 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -4,7 +4,6 @@ import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
@@ -13,13 +12,13 @@ import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
@@ -37,7 +36,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
@@ -59,22 +57,14 @@ class ForumManagerImpl implements ForumManager {
 			Logger.getLogger(ForumManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final ContactManager contactManager;
-	private final IdentityManager identityManager;
 	private final BdfReaderFactory bdfReaderFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
 
-	/** Ensures isolation between database reads and writes. */
-	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-
 	@Inject
-	ForumManagerImpl(DatabaseComponent db, ContactManager contactManager,
-			IdentityManager identityManager, BdfReaderFactory bdfReaderFactory,
+	ForumManagerImpl(DatabaseComponent db, BdfReaderFactory bdfReaderFactory,
 			MetadataEncoder metadataEncoder, MetadataParser metadataParser) {
 		this.db = db;
-		this.contactManager = contactManager;
-		this.identityManager = identityManager;
 		this.bdfReaderFactory = bdfReaderFactory;
 		this.metadataEncoder = metadataEncoder;
 		this.metadataParser = metadataParser;
@@ -87,7 +77,6 @@ class ForumManagerImpl implements ForumManager {
 
 	@Override
 	public void addLocalPost(ForumPost p) throws DbException {
-		lock.writeLock().lock();
 		try {
 			BdfDictionary d = new BdfDictionary();
 			d.put("timestamp", p.getMessage().getTimestamp());
@@ -105,45 +94,65 @@ class ForumManagerImpl implements ForumManager {
 			d.put("local", true);
 			d.put("read", true);
 			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
+			Transaction txn = db.startTransaction();
+			try {
+				db.addLocalMessage(txn, p.getMessage(), CLIENT_ID, meta, true);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
 	@Override
 	public Forum getForum(GroupId g) throws DbException {
-		lock.readLock().lock();
 		try {
-			return parseForum(db.getGroup(g));
+			Group group;
+			Transaction txn = db.startTransaction();
+			try {
+				group = db.getGroup(txn, g);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
+			return parseForum(group);
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<Forum> getForums() throws DbException {
-		lock.readLock().lock();
 		try {
+			Collection<Group> groups;
+			Transaction txn = db.startTransaction();
+			try {
+				groups = db.getGroups(txn, CLIENT_ID);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 			List<Forum> forums = new ArrayList<Forum>();
-			for (Group g : db.getGroups(CLIENT_ID)) forums.add(parseForum(g));
+			for (Group g : groups) forums.add(parseForum(g));
 			return Collections.unmodifiableList(forums);
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public byte[] getPostBody(MessageId m) throws DbException {
-		lock.readLock().lock();
 		try {
-			byte[] raw = db.getRawMessage(m);
+			byte[] raw;
+			Transaction txn = db.startTransaction();
+			try {
+				raw = db.getRawMessage(txn, m);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 			ByteArrayInputStream in = new ByteArrayInputStream(raw,
 					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 			BdfReader r = bdfReaderFactory.createReader(in);
@@ -164,74 +173,76 @@ class ForumManagerImpl implements ForumManager {
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<ForumPostHeader> getPostHeaders(GroupId g)
 			throws DbException {
-		lock.readLock().lock();
+		Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
+		Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
+		Map<MessageId, Metadata> metadata;
+		Transaction txn = db.startTransaction();
 		try {
 			// Load the IDs of the user's identities
-			Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
-			for (LocalAuthor a : identityManager.getLocalAuthors())
+			for (LocalAuthor a : db.getLocalAuthors(txn))
 				localAuthorIds.add(a.getId());
 			// Load the IDs of contacts' identities
-			Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
-			for (Contact c : contactManager.getContacts())
+			for (Contact c : db.getContacts(txn))
 				contactAuthorIds.add(c.getAuthor().getId());
-			// Load and parse the metadata
-			Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
-			Collection<ForumPostHeader> headers =
-					new ArrayList<ForumPostHeader>();
-			for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-				MessageId messageId = e.getKey();
-				Metadata meta = e.getValue();
-				try {
-					BdfDictionary d = metadataParser.parse(meta);
-					long timestamp = d.getInteger("timestamp");
-					Author author = null;
-					Author.Status authorStatus = ANONYMOUS;
-					BdfDictionary d1 = d.getDictionary("author", null);
-					if (d1 != null) {
-						AuthorId authorId = new AuthorId(d1.getRaw("id"));
-						String name = d1.getString("name");
-						byte[] publicKey = d1.getRaw("publicKey");
-						author = new Author(authorId, name, publicKey);
-						if (localAuthorIds.contains(authorId))
-							authorStatus = VERIFIED;
-						else if (contactAuthorIds.contains(authorId))
-							authorStatus = VERIFIED;
-						else authorStatus = UNKNOWN;
-					}
-					String contentType = d.getString("contentType");
-					boolean read = d.getBoolean("read");
-					headers.add(new ForumPostHeader(messageId, timestamp,
-							author, authorStatus, contentType, read));
-				} catch (FormatException ex) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, ex.toString(), ex);
+			// Load the metadata
+			metadata = db.getMessageMetadata(txn, g);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		// Parse the metadata
+		Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>();
+		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+			try {
+				BdfDictionary d = metadataParser.parse(e.getValue());
+				long timestamp = d.getInteger("timestamp");
+				Author author = null;
+				Author.Status authorStatus = ANONYMOUS;
+				BdfDictionary d1 = d.getDictionary("author", null);
+				if (d1 != null) {
+					AuthorId authorId = new AuthorId(d1.getRaw("id"));
+					String name = d1.getString("name");
+					byte[] publicKey = d1.getRaw("publicKey");
+					author = new Author(authorId, name, publicKey);
+					if (localAuthorIds.contains(authorId))
+						authorStatus = VERIFIED;
+					else if (contactAuthorIds.contains(authorId))
+						authorStatus = VERIFIED;
+					else authorStatus = UNKNOWN;
 				}
+				String contentType = d.getString("contentType");
+				boolean read = d.getBoolean("read");
+				headers.add(new ForumPostHeader(e.getKey(), timestamp, author,
+						authorStatus, contentType, read));
+			} catch (FormatException ex) {
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, ex.toString(), ex);
 			}
-			return headers;
-		} finally {
-			lock.readLock().unlock();
 		}
+		return headers;
 	}
 
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
-		lock.writeLock().lock();
 		try {
 			BdfDictionary d = new BdfDictionary();
 			d.put("read", read);
-			db.mergeMessageMetadata(m, metadataEncoder.encode(d));
+			Metadata meta = metadataEncoder.encode(d);
+			Transaction txn = db.startTransaction();
+			try {
+				db.mergeMessageMetadata(txn, m, meta);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
index 72efd00ac056eed7fbb1a24fae6816aa962adadd..6f8554ce8febc2c884215346d4119ac54592e4ee 100644
--- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
@@ -5,7 +5,6 @@ import com.google.inject.Inject;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -18,6 +17,7 @@ import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumSharingManager;
@@ -45,10 +45,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Logger;
 
-import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
@@ -62,11 +60,7 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 
 	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
 
-	private static final Logger LOG =
-			Logger.getLogger(ForumSharingManagerImpl.class.getName());
-
 	private final DatabaseComponent db;
-	private final ContactManager contactManager;
 	private final ForumManager forumManager;
 	private final GroupFactory groupFactory;
 	private final PrivateGroupFactory privateGroupFactory;
@@ -79,18 +73,14 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 	private final Clock clock;
 	private final Group localGroup;
 
-	/** Ensures isolation between database reads and writes. */
-	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-
 	@Inject
 	ForumSharingManagerImpl(DatabaseComponent db,
-			ContactManager contactManager, ForumManager forumManager,
-			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
+			ForumManager forumManager, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory,
 			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser, SecureRandom random, Clock clock) {
 		this.db = db;
-		this.contactManager = contactManager;
 		this.forumManager = forumManager;
 		this.groupFactory = groupFactory;
 		this.privateGroupFactory = privateGroupFactory;
@@ -106,57 +96,39 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 	}
 
 	@Override
-	public void addingContact(Contact c) {
-		lock.writeLock().lock();
+	public void addingContact(Transaction txn, Contact c) throws DbException {
 		try {
 			// Create a group to share with the contact
 			Group g = getContactGroup(c);
 			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibleToContact(c.getId(), g.getId(), true);
+			db.addGroup(txn, g);
+			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
 			// Attach the contact ID to the group
 			BdfDictionary d = new BdfDictionary();
 			d.put("contactId", c.getId().getInt());
-			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
+			db.mergeGroupMetadata(txn, g.getId(), metadataEncoder.encode(d));
 			// Share any forums that are shared with all contacts
-			List<Forum> shared = getForumsSharedWithAllContacts();
-			storeMessage(g.getId(), shared, 0);
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			List<Forum> shared = getForumsSharedWithAllContacts(txn);
+			storeMessage(txn, g.getId(), shared, 0);
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		} finally {
-			lock.writeLock().unlock();
+			throw new DbException(e);
 		}
 	}
 
 	@Override
-	public void removingContact(Contact c) {
-		lock.writeLock().lock();
-		try {
-			db.removeGroup(getContactGroup(c));
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	@Override
-	public void validatingMessage(Message m, ClientId c, Metadata meta) {
+	public void validatingMessage(Transaction txn, Message m, ClientId c,
+			Metadata meta) throws DbException {
 		if (c.equals(CLIENT_ID)) {
-			lock.writeLock().lock();
 			try {
-				ContactId contactId = getContactId(m.getGroupId());
-				setForumVisibility(contactId, getVisibleForums(m));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, e.toString(), e);
+				ContactId contactId = getContactId(txn, m.getGroupId());
+				setForumVisibility(txn, contactId, getVisibleForums(txn, m));
 			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, e.toString(), e);
-			} finally {
-				lock.writeLock().unlock();
+				throw new DbException(e);
 			}
 		}
 	}
@@ -179,149 +151,162 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 
 	@Override
 	public void addForum(Forum f) throws DbException {
-		lock.writeLock().lock();
+		Transaction txn = db.startTransaction();
 		try {
-			db.addGroup(f.getGroup());
+			db.addGroup(txn, f.getGroup());
+			txn.setComplete();
 		} finally {
-			lock.writeLock().unlock();
+			db.endTransaction(txn);
 		}
 	}
 
 	@Override
 	public void removeForum(Forum f) throws DbException {
-		lock.writeLock().lock();
 		try {
-			// Update the list of forums shared with each contact
-			for (Contact c : contactManager.getContacts()) {
-				Group contactGroup = getContactGroup(c);
-				removeFromList(contactGroup.getId(), f);
+			// Update the list shared with each contact
+			Transaction txn = db.startTransaction();
+			try {
+				for (Contact c : db.getContacts(txn))
+					removeFromList(txn, getContactGroup(c).getId(), f);
+				db.removeGroup(txn, f.getGroup());
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
-			db.removeGroup(f.getGroup());
 		} catch (IOException e) {
 			throw new DbException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<Forum> getAvailableForums() throws DbException {
-		lock.readLock().lock();
 		try {
-			// Get any forums we subscribe to
-			Set<Group> subscribed = new HashSet<Group>(db.getGroups(
-					forumManager.getClientId()));
-			// Get all forums shared by contacts
 			Set<Forum> available = new HashSet<Forum>();
-			for (Contact c : contactManager.getContacts()) {
-				Group g = getContactGroup(c);
-				// Find the latest update version
-				LatestUpdate latest = findLatest(g.getId(), false);
-				if (latest != null) {
-					// Retrieve and parse the latest update
-					byte[] raw = db.getRawMessage(latest.messageId);
-					for (Forum f : parseForumList(raw)) {
-						if (!subscribed.contains(f.getGroup()))
-							available.add(f);
+			Transaction txn = db.startTransaction();
+			try {
+				// Get any forums we subscribe to
+				Set<Group> subscribed = new HashSet<Group>(db.getGroups(txn,
+						forumManager.getClientId()));
+				// Get all forums shared by contacts
+				for (Contact c : db.getContacts(txn)) {
+					Group g = getContactGroup(c);
+					// Find the latest update version
+					LatestUpdate latest = findLatest(txn, g.getId(), false);
+					if (latest != null) {
+						// Retrieve and parse the latest update
+						byte[] raw = db.getRawMessage(txn, latest.messageId);
+						for (Forum f : parseForumList(raw)) {
+							if (!subscribed.contains(f.getGroup()))
+								available.add(f);
+						}
 					}
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 			return Collections.unmodifiableSet(available);
 		} catch (IOException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<Contact> getSharedBy(GroupId g) throws DbException {
-		lock.readLock().lock();
 		try {
 			List<Contact> subscribers = new ArrayList<Contact>();
-			for (Contact c : contactManager.getContacts()) {
-				Group contactGroup = getContactGroup(c);
-				if (listContains(contactGroup.getId(), g, false))
-					subscribers.add(c);
+			Transaction txn = db.startTransaction();
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					if (listContains(txn, getContactGroup(c).getId(), g, false))
+						subscribers.add(c);
+				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 			return Collections.unmodifiableList(subscribers);
 		} catch (IOException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<ContactId> getSharedWith(GroupId g) throws DbException {
-		lock.readLock().lock();
 		try {
 			List<ContactId> shared = new ArrayList<ContactId>();
-			for (Contact c : contactManager.getContacts()) {
-				Group contactGroup = getContactGroup(c);
-				if (listContains(contactGroup.getId(), g, true))
-					shared.add(c.getId());
+			Transaction txn = db.startTransaction();
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					if (listContains(txn, getContactGroup(c).getId(), g, true))
+						shared.add(c.getId());
+				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 			return Collections.unmodifiableList(shared);
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public void setSharedWith(GroupId g, Collection<ContactId> shared)
 			throws DbException {
-		lock.writeLock().lock();
 		try {
-			// Retrieve the forum
-			Forum f = parseForum(db.getGroup(g));
-			// Remove the forum from the list of forums shared with all contacts
-			removeFromList(localGroup.getId(), f);
-			// Update the list of forums shared with each contact
-			shared = new HashSet<ContactId>(shared);
-			for (Contact c : contactManager.getContacts()) {
-				Group contactGroup = getContactGroup(c);
-				if (shared.contains(c.getId())) {
-					if (addToList(contactGroup.getId(), f)) {
-						// If the contact is sharing the forum, make it visible
-						if (listContains(contactGroup.getId(), g, false))
-							db.setVisibleToContact(c.getId(), g, true);
+			Transaction txn = db.startTransaction();
+			try {
+				// Retrieve the forum
+				Forum f = parseForum(db.getGroup(txn, g));
+				// Remove the forum from the list shared with all contacts
+				removeFromList(txn, localGroup.getId(), f);
+				// Update the list shared with each contact
+				shared = new HashSet<ContactId>(shared);
+				for (Contact c : db.getContacts(txn)) {
+					Group cg = getContactGroup(c);
+					if (shared.contains(c.getId())) {
+						if (addToList(txn, cg.getId(), f)) {
+							if (listContains(txn, cg.getId(), g, false))
+								db.setVisibleToContact(txn, c.getId(), g, true);
+						}
+					} else {
+						removeFromList(txn, cg.getId(), f);
+						db.setVisibleToContact(txn, c.getId(), g, false);
 					}
-				} else {
-					removeFromList(contactGroup.getId(), f);
-					db.setVisibleToContact(c.getId(), g, false);
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
 	@Override
 	public void setSharedWithAll(GroupId g) throws DbException {
-		lock.writeLock().lock();
 		try {
-			// Retrieve the forum
-			Forum f = parseForum(db.getGroup(g));
-			// Add the forum to the list of forums shared with all contacts
-			addToList(localGroup.getId(), f);
-			// Add the forum to the list of forums shared with each contact
-			for (Contact c : contactManager.getContacts()) {
-				Group contactGroup = getContactGroup(c);
-				if (addToList(contactGroup.getId(), f)) {
-					// If the contact is sharing the forum, make it visible
-					if (listContains(contactGroup.getId(), g, false))
-						db.setVisibleToContact(getContactId(g), g, true);
+			Transaction txn = db.startTransaction();
+			try {
+				// Retrieve the forum
+				Forum f = parseForum(db.getGroup(txn, g));
+				// Add the forum to the list shared with all contacts
+				addToList(txn, localGroup.getId(), f);
+				// Add the forum to the list shared with each contact
+				for (Contact c : db.getContacts(txn)) {
+					Group cg = getContactGroup(c);
+					if (addToList(txn, cg.getId(), f)) {
+						if (listContains(txn, cg.getId(), g, false))
+							db.setVisibleToContact(txn, c.getId(), g, true);
+					}
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
@@ -329,23 +314,21 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
-	// Locking: lock.writeLock
-	private List<Forum> getForumsSharedWithAllContacts() throws DbException,
-			FormatException {
+	private List<Forum> getForumsSharedWithAllContacts(Transaction txn)
+			throws DbException, FormatException {
 		// Ensure the local group exists
-		db.addGroup(localGroup);
+		db.addGroup(txn, localGroup);
 		// Find the latest update in the local group
-		LatestUpdate latest = findLatest(localGroup.getId(), true);
+		LatestUpdate latest = findLatest(txn, localGroup.getId(), true);
 		if (latest == null) return Collections.emptyList();
 		// Retrieve and parse the latest update
-		return parseForumList(db.getRawMessage(latest.messageId));
+		return parseForumList(db.getRawMessage(txn, latest.messageId));
 	}
 
-	// Locking: lock.readLock
-	private LatestUpdate findLatest(GroupId g, boolean local)
+	private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
 			throws DbException, FormatException {
 		LatestUpdate latest = null;
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
 		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
 			BdfDictionary d = metadataParser.parse(e.getValue());
 			if (d.getBoolean("local") != local) continue;
@@ -384,16 +367,20 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		}
 	}
 
-	// Locking: lock.writeLock
-	private void storeMessage(GroupId g, List<Forum> forums, long version)
-			throws DbException, FormatException {
-		byte[] body = encodeForumList(forums, version);
-		long now = clock.currentTimeMillis();
-		Message m = messageFactory.createMessage(g, now, body);
-		BdfDictionary d = new BdfDictionary();
-		d.put("version", version);
-		d.put("local", true);
-		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), true);
+	private void storeMessage(Transaction txn, GroupId g, List<Forum> forums,
+			long version) throws DbException {
+		try {
+			byte[] body = encodeForumList(forums, version);
+			long now = clock.currentTimeMillis();
+			Message m = messageFactory.createMessage(g, now, body);
+			BdfDictionary d = new BdfDictionary();
+			d.put("version", version);
+			d.put("local", true);
+			db.addLocalMessage(txn, m, CLIENT_ID, metadataEncoder.encode(d),
+					true);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
 	}
 
 	private byte[] encodeForumList(List<Forum> forums, long version) {
@@ -418,23 +405,21 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		return out.toByteArray();
 	}
 
-	// Locking: lock.readLock
-	private ContactId getContactId(GroupId contactGroupId) throws DbException,
-			FormatException {
-		Metadata meta = db.getGroupMetadata(contactGroupId);
+	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
+			throws DbException, FormatException {
+		Metadata meta = db.getGroupMetadata(txn, contactGroupId);
 		BdfDictionary d = metadataParser.parse(meta);
 		return new ContactId(d.getInteger("contactId").intValue());
 	}
 
-	// Locking: lock.readLock
-	private Set<GroupId> getVisibleForums(Message remoteUpdate)
-			throws DbException, FormatException {
+	private Set<GroupId> getVisibleForums(Transaction txn,
+			Message remoteUpdate) throws DbException, FormatException {
 		// Get the latest local update
-		LatestUpdate local = findLatest(remoteUpdate.getGroupId(), true);
+		LatestUpdate local = findLatest(txn, remoteUpdate.getGroupId(), true);
 		// If there's no local update, no forums are visible
 		if (local == null) return Collections.emptySet();
 		// Intersect the sets of shared forums
-		byte[] localRaw = db.getRawMessage(local.messageId);
+		byte[] localRaw = db.getRawMessage(txn, local.messageId);
 		Set<Forum> shared = new HashSet<Forum>(parseForumList(localRaw));
 		shared.retainAll(parseForumList(remoteUpdate.getRaw()));
 		// Forums in the intersection should be visible
@@ -443,16 +428,15 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		return visible;
 	}
 
-	// Locking: lock.writeLock
-	private void setForumVisibility(ContactId c, Set<GroupId> visible)
-			throws DbException {
-		for (Group g : db.getGroups(forumManager.getClientId())) {
-			boolean isVisible = db.isVisibleToContact(c, g.getId());
+	private void setForumVisibility(Transaction txn, ContactId c,
+			Set<GroupId> visible) throws DbException {
+		for (Group g : db.getGroups(txn, forumManager.getClientId())) {
+			boolean isVisible = db.isVisibleToContact(txn, c, g.getId());
 			boolean shouldBeVisible = visible.contains(g.getId());
 			if (isVisible && !shouldBeVisible)
-				db.setVisibleToContact(c, g.getId(), false);
+				db.setVisibleToContact(txn, c, g.getId(), false);
 			else if (!isVisible && shouldBeVisible)
-				db.setVisibleToContact(c, g.getId(), true);
+				db.setVisibleToContact(txn, c, g.getId(), true);
 		}
 	}
 
@@ -491,38 +475,38 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		}
 	}
 
-	// Locking: lock.readLock
-	private boolean listContains(GroupId g, GroupId forum, boolean local)
-			throws DbException, FormatException {
-		LatestUpdate latest = findLatest(g, local);
+	private boolean listContains(Transaction txn, GroupId g, GroupId forum,
+			boolean local) throws DbException, FormatException {
+		LatestUpdate latest = findLatest(txn, g, local);
 		if (latest == null) return false;
-		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		byte[] raw = db.getRawMessage(txn, latest.messageId);
+		List<Forum> list = parseForumList(raw);
 		for (Forum f : list) if (f.getId().equals(forum)) return true;
 		return false;
 	}
 
-	// Locking: lock.writeLock
-	private boolean addToList(GroupId g, Forum f) throws DbException,
-			FormatException {
-		LatestUpdate latest = findLatest(g, true);
+	private boolean addToList(Transaction txn, GroupId g, Forum f)
+			throws DbException, FormatException {
+		LatestUpdate latest = findLatest(txn, g, true);
 		if (latest == null) {
-			storeMessage(g, Collections.singletonList(f), 0);
+			storeMessage(txn, g, Collections.singletonList(f), 0);
 			return true;
 		}
-		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		byte[] raw = db.getRawMessage(txn, latest.messageId);
+		List<Forum> list = parseForumList(raw);
 		if (list.contains(f)) return false;
 		list.add(f);
-		storeMessage(g, list, latest.version + 1);
+		storeMessage(txn, g, list, latest.version + 1);
 		return true;
 	}
 
-	// Locking: lock.writeLock
-	private void removeFromList(GroupId g, Forum f) throws DbException,
-			FormatException {
-		LatestUpdate latest = findLatest(g, true);
+	private void removeFromList(Transaction txn, GroupId g, Forum f)
+			throws DbException, FormatException {
+		LatestUpdate latest = findLatest(txn, g, true);
 		if (latest == null) return;
-		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
-		if (list.remove(f)) storeMessage(g, list, latest.version + 1);
+		byte[] raw = db.getRawMessage(txn, latest.messageId);
+		List<Forum> list = parseForumList(raw);
+		if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1);
 	}
 
 	private static class LatestUpdate {
diff --git a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
index 71b34eac925e31a79d413f9f228b51a43dd7e97a..475ead3e4a1321e7906a115b38a09c68f24e3319 100644
--- a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
+++ b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
@@ -5,6 +5,7 @@ import com.google.inject.Inject;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchLocalAuthorException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.LocalAuthorAddedEvent;
 import org.briarproject.api.event.LocalAuthorRemovedEvent;
@@ -47,19 +48,31 @@ class IdentityManagerImpl implements IdentityManager, Service {
 	public boolean start() {
 		// Finish adding/removing any partly added/removed pseudonyms
 		try {
-			for (LocalAuthor a : db.getLocalAuthors()) {
-				if (a.getStatus().equals(ADDING)) {
-					for (AddIdentityHook hook : addHooks)
-						hook.addingIdentity(a);
-					db.setLocalAuthorStatus(a.getId(), ACTIVE);
-					eventBus.broadcast(new LocalAuthorAddedEvent(a.getId()));
-				} else if (a.getStatus().equals(REMOVING)) {
-					for (RemoveIdentityHook hook : removeHooks)
-						hook.removingIdentity(a);
-					db.removeLocalAuthor(a.getId());
-					eventBus.broadcast(new LocalAuthorRemovedEvent(a.getId()));
+			List<AuthorId> added = new ArrayList<AuthorId>();
+			List<AuthorId> removed = new ArrayList<AuthorId>();
+			Transaction txn = db.startTransaction();
+			try {
+				for (LocalAuthor a : db.getLocalAuthors(txn)) {
+					if (a.getStatus().equals(ADDING)) {
+						for (AddIdentityHook hook : addHooks)
+							hook.addingIdentity(txn, a);
+						db.setLocalAuthorStatus(txn, a.getId(), ACTIVE);
+						added.add(a.getId());
+					} else if (a.getStatus().equals(REMOVING)) {
+						for (RemoveIdentityHook hook : removeHooks)
+							hook.removingIdentity(txn, a);
+						db.removeLocalAuthor(txn, a.getId());
+						removed.add(a.getId());
+					}
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
+			for (AuthorId a : added)
+				eventBus.broadcast(new LocalAuthorAddedEvent(a));
+			for (AuthorId a : removed)
+				eventBus.broadcast(new LocalAuthorRemovedEvent(a));
 			return true;
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -84,22 +97,43 @@ class IdentityManagerImpl implements IdentityManager, Service {
 
 	@Override
 	public void addLocalAuthor(LocalAuthor localAuthor) throws DbException {
-		db.addLocalAuthor(localAuthor);
-		for (AddIdentityHook hook : addHooks) hook.addingIdentity(localAuthor);
-		db.setLocalAuthorStatus(localAuthor.getId(), ACTIVE);
+		Transaction txn = db.startTransaction();
+		try {
+			db.addLocalAuthor(txn, localAuthor);
+			for (AddIdentityHook hook : addHooks)
+				hook.addingIdentity(txn, localAuthor);
+			db.setLocalAuthorStatus(txn, localAuthor.getId(), ACTIVE);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		eventBus.broadcast(new LocalAuthorAddedEvent(localAuthor.getId()));
 	}
 
 	@Override
 	public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
-		LocalAuthor author = db.getLocalAuthor(a);
+		LocalAuthor author;
+		Transaction txn = db.startTransaction();
+		try {
+			author = db.getLocalAuthor(txn, a);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		if (author.getStatus().equals(ACTIVE)) return author;
 		throw new NoSuchLocalAuthorException();
 	}
 
 	@Override
 	public Collection<LocalAuthor> getLocalAuthors() throws DbException {
-		Collection<LocalAuthor> authors = db.getLocalAuthors();
+		Collection<LocalAuthor> authors;
+		Transaction txn = db.startTransaction();
+		try {
+			authors = db.getLocalAuthors(txn);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		// Filter out any pseudonyms that are being added or removed
 		List<LocalAuthor> active = new ArrayList<LocalAuthor>(authors.size());
 		for (LocalAuthor a : authors)
@@ -109,11 +143,17 @@ class IdentityManagerImpl implements IdentityManager, Service {
 
 	@Override
 	public void removeLocalAuthor(AuthorId a) throws DbException {
-		LocalAuthor localAuthor = db.getLocalAuthor(a);
-		db.setLocalAuthorStatus(a, REMOVING);
-		for (RemoveIdentityHook hook : removeHooks)
-			hook.removingIdentity(localAuthor);
-		db.removeLocalAuthor(a);
+		Transaction txn = db.startTransaction();
+		try {
+			LocalAuthor localAuthor = db.getLocalAuthor(txn, a);
+			db.setLocalAuthorStatus(txn, a, REMOVING);
+			for (RemoveIdentityHook hook : removeHooks)
+				hook.removingIdentity(txn, localAuthor);
+			db.removeLocalAuthor(txn, a);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		eventBus.broadcast(new LocalAuthorRemovedEvent(a));
 	}
 }
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 5185cac6e8ae01e2250fd777257e04363e87a5f5..0441d27ce29533486735a94fab4b89b3e85da12f 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -5,7 +5,6 @@ import com.google.inject.Inject;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -16,7 +15,7 @@ import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
-import org.briarproject.api.db.NoSuchContactException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageHeader;
@@ -50,19 +49,17 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			Logger.getLogger(MessagingManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final BdfReaderFactory bdfReaderFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
 
 	@Inject
-	MessagingManagerImpl(DatabaseComponent db, ContactManager contactManager,
+	MessagingManagerImpl(DatabaseComponent db,
 			PrivateGroupFactory privateGroupFactory,
 			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser) {
 		this.db = db;
-		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
 		this.metadataEncoder = metadataEncoder;
@@ -70,19 +67,17 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	}
 
 	@Override
-	public void addingContact(Contact c) {
+	public void addingContact(Transaction txn, Contact c) throws DbException {
 		try {
 			// Create a group to share with the contact
 			Group g = getContactGroup(c);
 			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibleToContact(c.getId(), g.getId(), true);
+			db.addGroup(txn, g);
+			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
 			// Attach the contact ID to the group
 			BdfDictionary d = new BdfDictionary();
 			d.put("contactId", c.getId().getInt());
-			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			db.mergeGroupMetadata(txn, g.getId(), metadataEncoder.encode(d));
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -93,12 +88,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	}
 
 	@Override
-	public void removingContact(Contact c) {
-		try {
-			db.removeGroup(getContactGroup(c));
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	@Override
@@ -108,15 +99,22 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public void addLocalMessage(PrivateMessage m) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("timestamp", m.getMessage().getTimestamp());
-		if (m.getParent() != null) d.put("parent", m.getParent().getBytes());
-		d.put("contentType", m.getContentType());
-		d.put("local", true);
-		d.put("read", true);
 		try {
+			BdfDictionary d = new BdfDictionary();
+			d.put("timestamp", m.getMessage().getTimestamp());
+			if (m.getParent() != null)
+				d.put("parent", m.getParent().getBytes());
+			d.put("contentType", m.getContentType());
+			d.put("local", true);
+			d.put("read", true);
 			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(m.getMessage(), CLIENT_ID, meta, true);
+			Transaction txn = db.startTransaction();
+			try {
+				db.addLocalMessage(txn, m.getMessage(), CLIENT_ID, meta, true);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -125,26 +123,48 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public ContactId getContactId(GroupId g) throws DbException {
 		try {
-			BdfDictionary d = metadataParser.parse(db.getGroupMetadata(g));
-			long id = d.getInteger("contactId");
-			return new ContactId((int) id);
+			Metadata meta;
+			Transaction txn = db.startTransaction();
+			try {
+				meta = db.getGroupMetadata(txn, g);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
+			BdfDictionary d = metadataParser.parse(meta);
+			return new ContactId(d.getInteger("contactId").intValue());
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			throw new NoSuchContactException();
+			throw new DbException(e);
 		}
 	}
 
 	@Override
 	public GroupId getConversationId(ContactId c) throws DbException {
-		return getContactGroup(contactManager.getContact(c)).getId();
+		Contact contact;
+		Transaction txn = db.startTransaction();
+		try {
+			contact = db.getContact(txn, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return getContactGroup(contact).getId();
 	}
 
 	@Override
 	public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c)
 			throws DbException {
-		GroupId groupId = getConversationId(c);
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(groupId);
-		Collection<MessageStatus> statuses = db.getMessageStatus(c, groupId);
+		GroupId g = getConversationId(c);
+		Map<MessageId, Metadata> metadata;
+		Collection<MessageStatus> statuses;
+		Transaction txn = db.startTransaction();
+		try {
+			metadata = db.getMessageMetadata(txn, g);
+			statuses = db.getMessageStatus(txn, c, g);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		Collection<PrivateMessageHeader> headers =
 				new ArrayList<PrivateMessageHeader>();
 		for (MessageStatus s : statuses) {
@@ -168,7 +188,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public byte[] getMessageBody(MessageId m) throws DbException {
-		byte[] raw = db.getRawMessage(m);
+		byte[] raw;
+		Transaction txn = db.startTransaction();
+		try {
+			raw = db.getRawMessage(txn, m);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		ByteArrayInputStream in = new ByteArrayInputStream(raw,
 				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 		BdfReader r = bdfReaderFactory.createReader(in);
@@ -191,10 +218,17 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("read", read);
 		try {
-			db.mergeMessageMetadata(m, metadataEncoder.encode(d));
+			BdfDictionary d = new BdfDictionary();
+			d.put("read", read);
+			Metadata meta = metadataEncoder.encode(d);
+			Transaction txn = db.startTransaction();
+			try {
+				db.mergeMessageMetadata(txn, m, meta);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
index 66987c4fde649b5e11671d0727fb001ac1589619..bd110d0cf652715543b0b4670adfa7e7fc65f1ba 100644
--- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.TransportDisabledEvent;
 import org.briarproject.api.event.TransportEnabledEvent;
@@ -27,6 +28,7 @@ import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.api.properties.TransportPropertyManager;
 import org.briarproject.api.settings.Settings;
+import org.briarproject.api.settings.SettingsManager;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.ui.UiCallback;
 
@@ -60,6 +62,7 @@ class PluginManagerImpl implements PluginManager, Service {
 	private final DatabaseComponent db;
 	private final Poller poller;
 	private final ConnectionManager connectionManager;
+	private final SettingsManager settingsManager;
 	private final TransportPropertyManager transportPropertyManager;
 	private final UiCallback uiCallback;
 	private final Map<TransportId, Plugin> plugins;
@@ -72,6 +75,7 @@ class PluginManagerImpl implements PluginManager, Service {
 			DuplexPluginConfig duplexPluginConfig, Clock clock,
 			DatabaseComponent db, Poller poller,
 			ConnectionManager connectionManager,
+			SettingsManager settingsManager,
 			TransportPropertyManager transportPropertyManager,
 			UiCallback uiCallback) {
 		this.ioExecutor = ioExecutor;
@@ -82,6 +86,7 @@ class PluginManagerImpl implements PluginManager, Service {
 		this.db = db;
 		this.poller = poller;
 		this.connectionManager = connectionManager;
+		this.settingsManager = settingsManager;
 		this.transportPropertyManager = transportPropertyManager;
 		this.uiCallback = uiCallback;
 		plugins = new ConcurrentHashMap<TransportId, Plugin>();
@@ -181,7 +186,13 @@ class PluginManagerImpl implements PluginManager, Service {
 				}
 				try {
 					long start = clock.currentTimeMillis();
-					db.addTransport(id, plugin.getMaxLatency());
+					Transaction txn = db.startTransaction();
+					try {
+						db.addTransport(txn, id, plugin.getMaxLatency());
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
 					long duration = clock.currentTimeMillis() - start;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Adding transport took " + duration + " ms");
@@ -244,7 +255,13 @@ class PluginManagerImpl implements PluginManager, Service {
 				}
 				try {
 					long start = clock.currentTimeMillis();
-					db.addTransport(id, plugin.getMaxLatency());
+					Transaction txn = db.startTransaction();
+					try {
+						db.addTransport(txn, id, plugin.getMaxLatency());
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
 					long duration = clock.currentTimeMillis() - start;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Adding transport took " + duration + " ms");
@@ -319,7 +336,7 @@ class PluginManagerImpl implements PluginManager, Service {
 
 		public Settings getSettings() {
 			try {
-				return db.getSettings(id.getString());
+				return settingsManager.getSettings(id.getString());
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				return new Settings();
@@ -348,7 +365,7 @@ class PluginManagerImpl implements PluginManager, Service {
 
 		public void mergeSettings(Settings s) {
 			try {
-				db.mergeSettings(s, id.getString());
+				settingsManager.mergeSettings(s, id.getString());
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index a7babd5cdad32f7439e69f081d183e6b388dcf13..1032717f56425f05c1e38fc6e4dab714cbef8299 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -7,7 +7,6 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -21,6 +20,7 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.api.properties.TransportPropertyManager;
 import org.briarproject.api.sync.ClientId;
@@ -41,10 +41,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.logging.Logger;
 
-import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
@@ -57,11 +54,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 
 	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
 
-	private static final Logger LOG =
-			Logger.getLogger(TransportPropertyManagerImpl.class.getName());
-
 	private final DatabaseComponent db;
-	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final MessageFactory messageFactory;
 	private final BdfReaderFactory bdfReaderFactory;
@@ -71,18 +64,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	private final Clock clock;
 	private final Group localGroup;
 
-	/** Ensures isolation between database reads and writes. */
-	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-
 	@Inject
 	TransportPropertyManagerImpl(DatabaseComponent db,
-			ContactManager contactManager, GroupFactory groupFactory,
-			PrivateGroupFactory privateGroupFactory,
+			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
 			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser, Clock clock) {
 		this.db = db;
-		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.messageFactory = messageFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -95,165 +83,171 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	}
 
 	@Override
-	public void addingContact(Contact c) {
-		lock.writeLock().lock();
-		try {
-			// Create a group to share with the contact
-			Group g = getContactGroup(c);
-			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibleToContact(c.getId(), g.getId(), true);
-			// Copy the latest local properties into the group
-			DeviceId dev = db.getDeviceId();
-			Map<TransportId, TransportProperties> local = getLocalProperties();
-			for (Entry<TransportId, TransportProperties> e : local.entrySet()) {
-				storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 1, true,
-						true);
-			}
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		} finally {
-			lock.writeLock().unlock();
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		// Create a group to share with the contact
+		Group g = getContactGroup(c);
+		// Store the group and share it with the contact
+		db.addGroup(txn, g);
+		db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+		// Copy the latest local properties into the group
+		DeviceId dev = db.getDeviceId(txn);
+		Map<TransportId, TransportProperties> local = getLocalProperties();
+		for (Entry<TransportId, TransportProperties> e : local.entrySet()) {
+			storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 1,
+					true, true);
 		}
 	}
 
 	@Override
-	public void removingContact(Contact c) {
-		lock.writeLock().lock();
-		try {
-			db.removeGroup(getContactGroup(c));
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	@Override
 	public void addRemoteProperties(ContactId c, DeviceId dev,
 			Map<TransportId, TransportProperties> props) throws DbException {
-		lock.writeLock().lock();
+		Transaction txn = db.startTransaction();
 		try {
-			Group g = getContactGroup(contactManager.getContact(c));
+			Group g = getContactGroup(db.getContact(txn, c));
 			for (Entry<TransportId, TransportProperties> e : props.entrySet()) {
-				storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false,
-						false);
+				storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 0,
+						false, false);
 			}
-		} catch (FormatException e) {
-			throw new DbException(e);
+			txn.setComplete();
 		} finally {
-			lock.writeLock().unlock();
+			db.endTransaction(txn);
 		}
 	}
 
 	@Override
 	public Map<TransportId, TransportProperties> getLocalProperties()
 			throws DbException {
-		lock.readLock().lock();
 		try {
-			// Find the latest local update for each transport
-			Map<TransportId, LatestUpdate> latest =
-					findLatest(localGroup.getId(), true);
-			// Retrieve and parse the latest local properties
 			Map<TransportId, TransportProperties> local =
 					new HashMap<TransportId, TransportProperties>();
-			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
-				byte[] raw = db.getRawMessage(e.getValue().messageId);
-				local.put(e.getKey(), parseProperties(raw));
+			Transaction txn = db.startTransaction();
+			try {
+				// Find the latest local update for each transport
+				Map<TransportId, LatestUpdate> latest = findLatest(txn,
+						localGroup.getId(), true);
+				// Retrieve and parse the latest local properties
+				for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
+					byte[] raw = db.getRawMessage(txn, e.getValue().messageId);
+					local.put(e.getKey(), parseProperties(raw));
+				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 			return Collections.unmodifiableMap(local);
 		} catch (NoSuchGroupException e) {
 			// Local group doesn't exist - there are no local properties
 			return Collections.emptyMap();
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public TransportProperties getLocalProperties(TransportId t)
 			throws DbException {
-		lock.readLock().lock();
 		try {
-			// Find the latest local update
-			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
-			if (latest == null) return null;
-			// Retrieve and parse the latest local properties
-			return parseProperties(db.getRawMessage(latest.messageId));
+			TransportProperties p = null;
+			Transaction txn = db.startTransaction();
+			try {
+				// Find the latest local update
+				LatestUpdate latest = findLatest(txn, localGroup.getId(), t,
+						true);
+				if (latest != null) {
+					// Retrieve and parse the latest local properties
+					byte[] raw = db.getRawMessage(txn, latest.messageId);
+					p = parseProperties(raw);
+				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
+			return p;
 		} catch (NoSuchGroupException e) {
 			// Local group doesn't exist - there are no local properties
 			return null;
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			TransportId t) throws DbException {
-		lock.readLock().lock();
 		try {
 			Map<ContactId, TransportProperties> remote =
 					new HashMap<ContactId, TransportProperties>();
-			for (Contact c : contactManager.getContacts())  {
-				Group g = getContactGroup(c);
-				// Find the latest remote update
-				LatestUpdate latest = findLatest(g.getId(), t, false);
-				if (latest != null) {
-					// Retrieve and parse the latest remote properties
-					byte[] raw = db.getRawMessage(latest.messageId);
-					remote.put(c.getId(), parseProperties(raw));
+			Transaction txn = db.startTransaction();
+			try {
+				for (Contact c : db.getContacts(txn)) {
+					Group g = getContactGroup(c);
+					// Find the latest remote update
+					LatestUpdate latest = findLatest(txn, g.getId(), t, false);
+					if (latest != null) {
+						// Retrieve and parse the latest remote properties
+						byte[] raw = db.getRawMessage(txn, latest.messageId);
+						remote.put(c.getId(), parseProperties(raw));
+					}
 				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
 			return Collections.unmodifiableMap(remote);
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public void mergeLocalProperties(TransportId t, TransportProperties p)
 			throws DbException {
-		lock.writeLock().lock();
 		try {
-			// Create the local group if necessary
-			db.addGroup(localGroup);
-			// Merge the new properties with any existing properties
-			TransportProperties merged;
-			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
-			if (latest == null) {
-				merged = p;
-			} else {
-				byte[] raw = db.getRawMessage(latest.messageId);
-				TransportProperties old = parseProperties(raw);
-				merged = new TransportProperties(old);
-				merged.putAll(p);
-				if (merged.equals(old)) return; // Unchanged
-			}
-			// Store the merged properties in the local group
-			DeviceId dev = db.getDeviceId();
-			long version = latest == null ? 1 : latest.version + 1;
-			storeMessage(localGroup.getId(), dev, t, merged, version, true,
-					false);
-			// Store the merged properties in each contact's group
-			for (Contact c : contactManager.getContacts()) {
-				Group g = getContactGroup(c);
-				latest = findLatest(g.getId(), t, true);
-				version = latest == null ? 1 : latest.version + 1;
-				storeMessage(g.getId(), dev, t, merged, version, true, true);
+			Transaction txn = db.startTransaction();
+			try {
+				// Create the local group if necessary
+				db.addGroup(txn, localGroup);
+				// Merge the new properties with any existing properties
+				TransportProperties merged;
+				boolean changed;
+				LatestUpdate latest = findLatest(txn, localGroup.getId(), t,
+						true);
+				if (latest == null) {
+					merged = p;
+					changed = true;
+				} else {
+					byte[] raw = db.getRawMessage(txn, latest.messageId);
+					TransportProperties old = parseProperties(raw);
+					merged = new TransportProperties(old);
+					merged.putAll(p);
+					changed = !merged.equals(old);
+				}
+				if (changed) {
+					// Store the merged properties in the local group
+					DeviceId dev = db.getDeviceId(txn);
+					long version = latest == null ? 1 : latest.version + 1;
+					storeMessage(txn, localGroup.getId(), dev, t, merged,
+							version, true, false);
+					// Store the merged properties in each contact's group
+					for (Contact c : db.getContacts(txn)) {
+						Group g = getContactGroup(c);
+						latest = findLatest(txn, g.getId(), t, true);
+						version = latest == null ? 1 : latest.version + 1;
+						storeMessage(txn, g.getId(), dev, t, merged, version,
+								true, true);
+					}
+				}
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
 			}
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
-		} finally {
-			lock.writeLock().unlock();
 		}
 	}
 
@@ -261,18 +255,22 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
-	// Locking: lock.writeLock
-	private void storeMessage(GroupId g, DeviceId dev, TransportId t,
-			TransportProperties p, long version, boolean local, boolean shared)
-			throws DbException, FormatException {
-		byte[] body = encodeProperties(dev, t, p, version);
-		long now = clock.currentTimeMillis();
-		Message m = messageFactory.createMessage(g, now, body);
-		BdfDictionary d = new BdfDictionary();
-		d.put("transportId", t.getString());
-		d.put("version", version);
-		d.put("local", local);
-		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared);
+	private void storeMessage(Transaction txn, GroupId g, DeviceId dev,
+			TransportId t, TransportProperties p, long version, boolean local,
+			boolean shared) throws DbException {
+		try {
+			byte[] body = encodeProperties(dev, t, p, version);
+			long now = clock.currentTimeMillis();
+			Message m = messageFactory.createMessage(g, now, body);
+			BdfDictionary d = new BdfDictionary();
+			d.put("transportId", t.getString());
+			d.put("version", version);
+			d.put("local", local);
+			Metadata meta = metadataEncoder.encode(d);
+			db.addLocalMessage(txn, m, CLIENT_ID, meta, shared);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
 	}
 
 	private byte[] encodeProperties(DeviceId dev, TransportId t,
@@ -293,12 +291,11 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return out.toByteArray();
 	}
 
-	// Locking: lock.readLock
-	private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
-			throws DbException, FormatException {
+	private Map<TransportId, LatestUpdate> findLatest(Transaction txn,
+			GroupId g, boolean local) throws DbException, FormatException {
 		Map<TransportId, LatestUpdate> latestUpdates =
 				new HashMap<TransportId, LatestUpdate>();
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
 		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
 			BdfDictionary d = metadataParser.parse(e.getValue());
 			if (d.getBoolean("local") == local) {
@@ -312,11 +309,10 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return latestUpdates;
 	}
 
-	// Locking: lock.readLock
-	private LatestUpdate findLatest(GroupId g, TransportId t, boolean local)
-			throws DbException, FormatException {
+	private LatestUpdate findLatest(Transaction txn, GroupId g, TransportId t,
+			boolean local) throws DbException, FormatException {
 		LatestUpdate latest = null;
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
 		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
 			BdfDictionary d = metadataParser.parse(e.getValue());
 			if (d.getString("transportId").equals(t.getString())
@@ -330,25 +326,32 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	}
 
 	private TransportProperties parseProperties(byte[] raw)
-			throws IOException {
+			throws FormatException {
 		TransportProperties p = new TransportProperties();
 		ByteArrayInputStream in = new ByteArrayInputStream(raw,
 				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 		BdfReader r = bdfReaderFactory.createReader(in);
-		r.readListStart();
-		r.skipRaw(); // Device ID
-		r.skipString(); // Transport ID
-		r.skipInteger(); // Version
-		r.readDictionaryStart();
-		while (!r.hasDictionaryEnd()) {
-			String key = r.readString(MAX_PROPERTY_LENGTH);
-			String value = r.readString(MAX_PROPERTY_LENGTH);
-			p.put(key, value);
+		try {
+			r.readListStart();
+			r.skipRaw(); // Device ID
+			r.skipString(); // Transport ID
+			r.skipInteger(); // Version
+			r.readDictionaryStart();
+			while (!r.hasDictionaryEnd()) {
+				String key = r.readString(MAX_PROPERTY_LENGTH);
+				String value = r.readString(MAX_PROPERTY_LENGTH);
+				p.put(key, value);
+			}
+			r.readDictionaryEnd();
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return p;
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayInputStream
+			throw new RuntimeException(e);
 		}
-		r.readDictionaryEnd();
-		r.readListEnd();
-		if (!r.eof()) throw new FormatException();
-		return p;
 	}
 
 	private static class LatestUpdate {
diff --git a/briar-core/src/org/briarproject/settings/SettingsManagerImpl.java b/briar-core/src/org/briarproject/settings/SettingsManagerImpl.java
index ee7a3ed681ae900589170f53b03176932482e431..639d822bbc0c57c0eed488a6f509e9fa493f58af 100644
--- a/briar-core/src/org/briarproject/settings/SettingsManagerImpl.java
+++ b/briar-core/src/org/briarproject/settings/SettingsManagerImpl.java
@@ -4,6 +4,7 @@ import com.google.inject.Inject;
 
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.settings.Settings;
 import org.briarproject.api.settings.SettingsManager;
 
@@ -18,11 +19,24 @@ class SettingsManagerImpl implements SettingsManager {
 
 	@Override
 	public Settings getSettings(String namespace) throws DbException {
-		return db.getSettings(namespace);
+		Transaction txn = db.startTransaction();
+		try {
+			Settings s = db.getSettings(txn, namespace);
+			txn.setComplete();
+			return s;
+		} finally {
+			db.endTransaction(txn);
+		}
 	}
 
 	@Override
 	public void mergeSettings(Settings s, String namespace) throws DbException {
-		db.mergeSettings(s, namespace);
+		Transaction txn = db.startTransaction();
+		try {
+			db.mergeSettings(txn, s, namespace);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
index d0af71201dcc29592b158a590f55028b7d391c7a..8976eb7d16be1f951098999468f57a810740341c 100644
--- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
@@ -4,6 +4,7 @@ import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
@@ -50,8 +51,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 
 	private static final ThrowingRunnable<IOException> CLOSE =
 			new ThrowingRunnable<IOException>() {
-		public void run() {}
-	};
+				public void run() {}
+			};
 
 	private final DatabaseComponent db;
 	private final Executor dbExecutor;
@@ -178,7 +179,14 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Ack a = db.generateAck(contactId, MAX_MESSAGE_IDS);
+				Ack a;
+				Transaction txn = db.startTransaction();
+				try {
+					a = db.generateAck(txn, contactId, MAX_MESSAGE_IDS);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated ack: " + (a != null));
 				if (a != null) writerTasks.add(new WriteAck(a));
@@ -212,8 +220,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Collection<byte[]> b = db.generateRequestedBatch(contactId,
-						MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
+				Collection<byte[]> b;
+				Transaction txn = db.startTransaction();
+				try {
+					b = db.generateRequestedBatch(txn, contactId,
+							MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated batch: " + (b != null));
 				if (b != null) writerTasks.add(new WriteBatch(b));
@@ -247,8 +262,15 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Offer o = db.generateOffer(contactId, MAX_MESSAGE_IDS,
-						maxLatency);
+				Offer o;
+				Transaction txn = db.startTransaction();
+				try {
+					o = db.generateOffer(txn, contactId, MAX_MESSAGE_IDS,
+							maxLatency);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated offer: " + (o != null));
 				if (o != null) writerTasks.add(new WriteOffer(o));
@@ -282,7 +304,14 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Request r = db.generateRequest(contactId, MAX_MESSAGE_IDS);
+				Request r;
+				Transaction txn = db.startTransaction();
+				try {
+					r = db.generateRequest(txn, contactId, MAX_MESSAGE_IDS);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated request: " + (r != null));
 				if (r != null) writerTasks.add(new WriteRequest(r));
diff --git a/briar-core/src/org/briarproject/sync/IncomingSession.java b/briar-core/src/org/briarproject/sync/IncomingSession.java
index 6fbfdd2426ce1938f665f83d7dad59124b0dd22d..5e5f4c6df220bcfb23ef55b6417d07eb8f79ed65 100644
--- a/briar-core/src/org/briarproject/sync/IncomingSession.java
+++ b/briar-core/src/org/briarproject/sync/IncomingSession.java
@@ -5,6 +5,7 @@ import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
@@ -24,7 +25,9 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
 
-/** An incoming {@link org.briarproject.api.sync.SyncSession SyncSession}. */
+/**
+ * An incoming {@link org.briarproject.api.sync.SyncSession SyncSession}.
+ */
 class IncomingSession implements SyncSession, EventListener {
 
 	private static final Logger LOG =
@@ -103,7 +106,13 @@ class IncomingSession implements SyncSession, EventListener {
 
 		public void run() {
 			try {
-				db.receiveAck(contactId, ack);
+				Transaction txn = db.startTransaction();
+				try {
+					db.receiveAck(txn, contactId, ack);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				interrupt();
@@ -121,7 +130,13 @@ class IncomingSession implements SyncSession, EventListener {
 
 		public void run() {
 			try {
-				db.receiveMessage(contactId, message);
+				Transaction txn = db.startTransaction();
+				try {
+					db.receiveMessage(txn, contactId, message);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				interrupt();
@@ -139,7 +154,13 @@ class IncomingSession implements SyncSession, EventListener {
 
 		public void run() {
 			try {
-				db.receiveOffer(contactId, offer);
+				Transaction txn = db.startTransaction();
+				try {
+					db.receiveOffer(txn, contactId, offer);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				interrupt();
@@ -157,7 +178,13 @@ class IncomingSession implements SyncSession, EventListener {
 
 		public void run() {
 			try {
-				db.receiveRequest(contactId, request);
+				Transaction txn = db.startTransaction();
+				try {
+					db.receiveRequest(txn, contactId, request);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				interrupt();
diff --git a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
index 620e23a4b76b64f527fe6fadd7f4220be64d7557..31ca6268485c4f18b2de9f1c469e1a579287120d 100644
--- a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java
@@ -4,6 +4,7 @@ import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
@@ -40,8 +41,8 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 
 	private static final ThrowingRunnable<IOException> CLOSE =
 			new ThrowingRunnable<IOException>() {
-		public void run() {}
-	};
+				public void run() {}
+			};
 
 	private final DatabaseComponent db;
 	private final Executor dbExecutor;
@@ -119,7 +120,14 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Ack a = db.generateAck(contactId, MAX_MESSAGE_IDS);
+				Ack a;
+				Transaction txn = db.startTransaction();
+				try {
+					a = db.generateAck(txn, contactId, MAX_MESSAGE_IDS);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated ack: " + (a != null));
 				if (a == null) decrementOutstandingQueries();
@@ -154,8 +162,15 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 		public void run() {
 			if (interrupted) return;
 			try {
-				Collection<byte[]> b = db.generateBatch(contactId,
-						MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
+				Collection<byte[]> b;
+				Transaction txn = db.startTransaction();
+				try {
+					b = db.generateBatch(txn, contactId,
+							MAX_PACKET_PAYLOAD_LENGTH, maxLatency);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 				if (LOG.isLoggable(INFO))
 					LOG.info("Generated batch: " + (b != null));
 				if (b == null) decrementOutstandingQueries();
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 5fca1458452be5ed09fc776e6de3d3f3b1c1f773..e075f341f58a4633d5f45223a57af96c659a4573 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -9,7 +9,7 @@ import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchGroupException;
-import org.briarproject.api.db.NoSuchMessageException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.MessageAddedEvent;
@@ -82,14 +82,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
 			public void run() {
 				try {
 					// TODO: Don't do all of this in a single DB task
-					for (MessageId id : db.getMessagesToValidate(c)) {
-						try {
-							Message m = parseMessage(id, db.getRawMessage(id));
-							Group g = db.getGroup(m.getGroupId());
+					Transaction txn = db.startTransaction();
+					try {
+						for (MessageId id : db.getMessagesToValidate(txn, c)) {
+							byte[] raw = db.getRawMessage(txn, id);
+							Message m = parseMessage(id, raw);
+							Group g = db.getGroup(txn, m.getGroupId());
 							validateMessage(m, g);
-						} catch (NoSuchMessageException e) {
-							LOG.info("Message removed before validation");
 						}
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
 					}
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -127,17 +130,21 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					if (meta == null) {
-						db.setMessageValid(m, c, false);
-					} else {
-						for (ValidationHook hook : hooks)
-							hook.validatingMessage(m, c, meta);
-						db.mergeMessageMetadata(m.getId(), meta);
-						db.setMessageValid(m, c, true);
-						db.setMessageShared(m, true);
+					Transaction txn = db.startTransaction();
+					try {
+						if (meta == null) {
+							db.setMessageValid(txn, m, c, false);
+						} else {
+							for (ValidationHook hook : hooks)
+								hook.validatingMessage(txn, m, c, meta);
+							db.mergeMessageMetadata(txn, m.getId(), meta);
+							db.setMessageValid(txn, m, c, true);
+							db.setMessageShared(txn, m, true);
+						}
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
 					}
-				} catch (NoSuchMessageException e) {
-					LOG.info("Message removed during validation");
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -159,7 +166,13 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					validateMessage(m, db.getGroup(m.getGroupId()));
+					Transaction txn = db.startTransaction();
+					try {
+						validateMessage(m, db.getGroup(txn, m.getGroupId()));
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
 				} catch (NoSuchGroupException e) {
 					LOG.info("Group removed before validation");
 				} catch (DbException e) {
diff --git a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
index b1fa018fc63caae5a1bee6bcf0a2a04fb154c947..7e31a877c02f00f1e1f819d1c20dd485b7943ed7 100644
--- a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
+++ b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
@@ -7,6 +7,7 @@ import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
@@ -55,7 +56,14 @@ class KeyManagerImpl implements KeyManager, Service, EventListener {
 	@Override
 	public boolean start() {
 		try {
-			Map<TransportId, Integer> latencies = db.getTransportLatencies();
+			Map<TransportId, Integer> latencies;
+			Transaction txn = db.startTransaction();
+			try {
+				latencies = db.getTransportLatencies(txn);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 			for (Entry<TransportId, Integer> e : latencies.entrySet())
 				addTransport(e.getKey(), e.getValue());
 		} catch (DbException e) {
diff --git a/briar-core/src/org/briarproject/transport/TransportKeyManager.java b/briar-core/src/org/briarproject/transport/TransportKeyManager.java
index 2df96ccc71dc88fb2d19e8d4ea1574d740286588..ce15995ac47fc18d8a16ae8e2c1d6fa2fecc27e7 100644
--- a/briar-core/src/org/briarproject/transport/TransportKeyManager.java
+++ b/briar-core/src/org/briarproject/transport/TransportKeyManager.java
@@ -7,6 +7,7 @@ import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.system.Timer;
 import org.briarproject.api.transport.StreamContext;
@@ -66,7 +67,13 @@ class TransportKeyManager extends TimerTask {
 			// Load the transport keys from the DB
 			Map<ContactId, TransportKeys> loaded;
 			try {
-				loaded = db.getTransportKeys(transportId);
+				Transaction txn = db.startTransaction();
+				try {
+					loaded = db.getTransportKeys(txn, transportId);
+					txn.setComplete();
+				} finally {
+					db.endTransaction(txn);
+				}
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				return;
@@ -90,7 +97,13 @@ class TransportKeyManager extends TimerTask {
 			for (Entry<ContactId, TransportKeys> e : current.entrySet())
 				addKeys(e.getKey(), new MutableTransportKeys(e.getValue()));
 			// Write any rotated keys back to the DB
-			db.updateTransportKeys(rotated);
+			Transaction txn = db.startTransaction();
+			try {
+				db.updateTransportKeys(txn, rotated);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} finally {
@@ -135,7 +148,13 @@ class TransportKeyManager extends TimerTask {
 			// Initialise mutable state for the contact
 			addKeys(c, new MutableTransportKeys(k));
 			// Write the keys back to the DB
-			db.addTransportKeys(c, k);
+			Transaction txn = db.startTransaction();
+			try {
+				db.addTransportKeys(txn, c, k);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} finally {
@@ -171,8 +190,14 @@ class TransportKeyManager extends TimerTask {
 					outKeys.getStreamCounter());
 			// Increment the stream counter and write it back to the DB
 			outKeys.incrementStreamCounter();
-			db.incrementStreamCounter(c, transportId,
-					outKeys.getRotationPeriod());
+			Transaction txn = db.startTransaction();
+			try {
+				db.incrementStreamCounter(txn, c, transportId,
+						outKeys.getRotationPeriod());
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 			return ctx;
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -210,9 +235,15 @@ class TransportKeyManager extends TimerTask {
 				inContexts.remove(new Bytes(removeTag));
 			}
 			// Write the window back to the DB
-			db.setReorderingWindow(tagCtx.contactId, transportId,
-					inKeys.getRotationPeriod(), window.getBase(),
-					window.getBitmap());
+			Transaction txn = db.startTransaction();
+			try {
+				db.setReorderingWindow(txn, tagCtx.contactId, transportId,
+						inKeys.getRotationPeriod(), window.getBase(),
+						window.getBitmap());
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 			return ctx;
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -249,7 +280,13 @@ class TransportKeyManager extends TimerTask {
 			for (Entry<ContactId, TransportKeys> e : current.entrySet())
 				addKeys(e.getKey(), new MutableTransportKeys(e.getValue()));
 			// Write any rotated keys back to the DB
-			db.updateTransportKeys(rotated);
+			Transaction txn = db.startTransaction();
+			try {
+				db.updateTransportKeys(txn, rotated);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} finally {
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 0743df9495bca6e22c29329ac6454fa863ac2dc1..48e6ea037b05ad57cf77e2a91952323e5f4d0385 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -14,6 +14,7 @@ import org.briarproject.api.db.NoSuchLocalAuthorException;
 import org.briarproject.api.db.NoSuchMessageException;
 import org.briarproject.api.db.NoSuchTransportException;
 import org.briarproject.api.db.StorageStatus;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.GroupAddedEvent;
 import org.briarproject.api.event.GroupRemovedEvent;
@@ -50,11 +51,13 @@ import org.junit.Test;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
 
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
 import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
+import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -121,14 +124,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			exactly(9).of(database).startTransaction();
-			will(returnValue(txn));
-			exactly(9).of(database).commitTransaction(txn);
 			// open()
 			oneOf(database).open();
 			will(returnValue(false));
 			oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
 			will(returnValue(shutdownHandle));
+			// startTransaction()
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
 			// addLocalAuthor()
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(false));
@@ -171,6 +174,8 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(true));
 			oneOf(database).removeLocalAuthor(txn, localAuthorId);
+			// endTransaction()
+			oneOf(database).commitTransaction(txn);
 			// close()
 			oneOf(shutdown).removeShutdownHook(shutdownHandle);
 			oneOf(database).close();
@@ -179,15 +184,21 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 				shutdown);
 
 		assertFalse(db.open());
-		db.addLocalAuthor(localAuthor);
-		assertEquals(contactId, db.addContact(author, localAuthorId));
-		assertEquals(Collections.singletonList(contact), db.getContacts());
-		db.addGroup(group); // First time - listeners called
-		db.addGroup(group); // Second time - not called
-		assertEquals(Collections.singletonList(group), db.getGroups(clientId));
-		db.removeGroup(group);
-		db.removeContact(contactId);
-		db.removeLocalAuthor(localAuthorId);
+		Transaction transaction = db.startTransaction();
+		db.addLocalAuthor(transaction, localAuthor);
+		assertEquals(contactId,
+				db.addContact(transaction, author, localAuthorId));
+		assertEquals(Collections.singletonList(contact),
+				db.getContacts(transaction));
+		db.addGroup(transaction, group); // First time - listeners called
+		db.addGroup(transaction, group); // Second time - not called
+		assertEquals(Collections.singletonList(group),
+				db.getGroups(transaction, clientId));
+		db.removeGroup(transaction, group);
+		db.removeContact(transaction, contactId);
+		db.removeLocalAuthor(transaction, localAuthorId);
+		transaction.setComplete();
+		db.endTransaction(transaction);
 		db.close();
 
 		context.assertIsSatisfied();
@@ -211,11 +222,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
+		Transaction transaction = db.startTransaction();
 		try {
-			db.addLocalMessage(message, clientId, metadata, true);
+			db.addLocalMessage(transaction, message, clientId, metadata, true);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -253,7 +267,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.addLocalMessage(message, clientId, metadata, true);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.addLocalMessage(transaction, message, clientId, metadata, true);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -277,126 +297,178 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
+		Transaction transaction = db.startTransaction();
 		try {
-			db.addTransportKeys(contactId, createTransportKeys());
+			db.addTransportKeys(transaction, contactId, createTransportKeys());
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.generateAck(contactId, 123);
+			db.generateAck(transaction, contactId, 123);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.generateBatch(contactId, 123, 456);
+			db.generateBatch(transaction, contactId, 123, 456);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.generateOffer(contactId, 123, 456);
+			db.generateOffer(transaction, contactId, 123, 456);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.generateRequest(contactId, 123);
+			db.generateRequest(transaction, contactId, 123);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getContact(contactId);
+			db.getContact(transaction, contactId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getMessageStatus(contactId, groupId);
+			db.getMessageStatus(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getMessageStatus(contactId, messageId);
+			db.getMessageStatus(transaction, contactId, messageId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.incrementStreamCounter(contactId, transportId, 0);
+			db.incrementStreamCounter(transaction, contactId, transportId, 0);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.isVisibleToContact(contactId, groupId);
+			db.isVisibleToContact(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
 			Ack a = new Ack(Collections.singletonList(messageId));
-			db.receiveAck(contactId, a);
+			db.receiveAck(transaction, contactId, a);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.receiveMessage(contactId, message);
+			db.receiveMessage(transaction, contactId, message);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
 			Offer o = new Offer(Collections.singletonList(messageId));
-			db.receiveOffer(contactId, o);
+			db.receiveOffer(transaction, contactId, o);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
 			Request r = new Request(Collections.singletonList(messageId));
-			db.receiveRequest(contactId, r);
+			db.receiveRequest(transaction, contactId, r);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.removeContact(contactId);
+			db.removeContact(transaction, contactId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setReorderingWindow(contactId, transportId, 0, 0, new byte[4]);
+			db.setReorderingWindow(transaction, contactId, transportId, 0, 0,
+					new byte[REORDERING_WINDOW_SIZE / 8]);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setVisibleToContact(contactId, groupId, true);
+			db.setVisibleToContact(transaction, contactId, groupId, true);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -421,25 +493,34 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
+		Transaction transaction = db.startTransaction();
 		try {
-			db.addContact(author, localAuthorId);
+			db.addContact(transaction, author, localAuthorId);
 			fail();
 		} catch (NoSuchLocalAuthorException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getLocalAuthor(localAuthorId);
+			db.getLocalAuthor(transaction, localAuthorId);
 			fail();
 		} catch (NoSuchLocalAuthorException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.removeLocalAuthor(localAuthorId);
+			db.removeLocalAuthor(transaction, localAuthorId);
 			fail();
 		} catch (NoSuchLocalAuthorException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -468,53 +549,74 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
+		Transaction transaction = db.startTransaction();
 		try {
-			db.getGroup(groupId);
+			db.getGroup(transaction, groupId);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getGroupMetadata(groupId);
+			db.getGroupMetadata(transaction, groupId);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getMessageStatus(contactId, groupId);
+			db.getMessageStatus(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.isVisibleToContact(contactId, groupId);
+			db.isVisibleToContact(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.mergeGroupMetadata(groupId, metadata);
+			db.mergeGroupMetadata(transaction, groupId, metadata);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.removeGroup(group);
+			db.removeGroup(transaction, group);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setVisibleToContact(contactId, groupId, true);
+			db.setVisibleToContact(transaction, contactId, groupId, true);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -542,60 +644,84 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
+		Transaction transaction = db.startTransaction();
 		try {
-			db.deleteMessage(messageId);
+			db.deleteMessage(transaction, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.deleteMessageMetadata(messageId);
+			db.deleteMessageMetadata(transaction, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getRawMessage(messageId);
+			db.getRawMessage(transaction, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getMessageMetadata(messageId);
+			db.getMessageMetadata(transaction, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getMessageStatus(contactId, messageId);
+			db.getMessageStatus(transaction, contactId, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.mergeMessageMetadata(messageId, metadata);
+			db.mergeMessageMetadata(transaction, messageId, metadata);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setMessageShared(message, true);
+			db.setMessageShared(transaction, message, true);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setMessageValid(message, clientId, true);
+			db.setMessageValid(transaction, message, clientId, true);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -610,22 +736,21 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			// addLocalAuthor()
+			// startTransaction()
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			// addLocalAuthor()
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(false));
 			oneOf(database).addLocalAuthor(txn, localAuthor);
-			oneOf(database).commitTransaction(txn);
 			// addContact()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(true));
 			oneOf(database).containsContact(txn, authorId, localAuthorId);
 			will(returnValue(false));
 			oneOf(database).addContact(txn, author, localAuthorId);
 			will(returnValue(contactId));
+			// endTransaction()
 			oneOf(database).commitTransaction(txn);
 			// Check whether the transport is in the DB (which it's not)
 			exactly(4).of(database).startTransaction();
@@ -639,35 +764,55 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.addLocalAuthor(localAuthor);
-		assertEquals(contactId, db.addContact(author, localAuthorId));
+		Transaction transaction = db.startTransaction();
+		try {
+			db.addLocalAuthor(transaction, localAuthor);
+			assertEquals(contactId,
+					db.addContact(transaction, author, localAuthorId));
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
+		transaction = db.startTransaction();
 		try {
-			db.getTransportKeys(transportId);
+			db.getTransportKeys(transaction, transportId);
 			fail();
 		} catch (NoSuchTransportException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.incrementStreamCounter(contactId, transportId, 0);
+			db.incrementStreamCounter(transaction, contactId, transportId, 0);
 			fail();
 		} catch (NoSuchTransportException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.removeTransport(transportId);
+			db.removeTransport(transaction, transportId);
 			fail();
 		} catch (NoSuchTransportException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.setReorderingWindow(contactId, transportId, 0, 0, new byte[4]);
+			db.setReorderingWindow(transaction, contactId, transportId, 0, 0,
+					new byte[REORDERING_WINDOW_SIZE / 8]);
 			fail();
 		} catch (NoSuchTransportException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -695,8 +840,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		Ack a = db.generateAck(contactId, 123);
-		assertEquals(messagesToAck, a.getMessageIds());
+		Transaction transaction = db.startTransaction();
+		try {
+			Ack a = db.generateAck(transaction, contactId, 123);
+			assertEquals(messagesToAck, a.getMessageIds());
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -733,8 +884,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		assertEquals(messages, db.generateBatch(contactId, size * 2,
-				maxLatency));
+		Transaction transaction = db.startTransaction();
+		try {
+			assertEquals(messages, db.generateBatch(transaction, contactId,
+					size * 2, maxLatency));
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -764,8 +921,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		Offer o = db.generateOffer(contactId, 123, maxLatency);
-		assertEquals(ids, o.getMessageIds());
+		Transaction transaction = db.startTransaction();
+		try {
+			Offer o = db.generateOffer(transaction, contactId, 123, maxLatency);
+			assertEquals(ids, o.getMessageIds());
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -792,8 +955,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		Request r = db.generateRequest(contactId, 123);
-		assertEquals(ids, r.getMessageIds());
+		Transaction transaction = db.startTransaction();
+		try {
+			Request r = db.generateRequest(transaction, contactId, 123);
+			assertEquals(ids, r.getMessageIds());
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -831,8 +1000,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		assertEquals(messages, db.generateRequestedBatch(contactId, size * 2,
-				maxLatency));
+		Transaction transaction = db.startTransaction();
+		try {
+			assertEquals(messages, db.generateRequestedBatch(transaction,
+					contactId, size * 2, maxLatency));
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -858,7 +1033,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.receiveAck(contactId, new Ack(Collections.singletonList(messageId)));
+		Transaction transaction = db.startTransaction();
+		try {
+			Ack a = new Ack(Collections.singletonList(messageId));
+			db.receiveAck(transaction, contactId, a);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -896,7 +1078,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.receiveMessage(contactId, message);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.receiveMessage(transaction, contactId, message);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -926,7 +1114,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.receiveMessage(contactId, message);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.receiveMessage(transaction, contactId, message);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -952,7 +1146,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.receiveMessage(contactId, message);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.receiveMessage(transaction, contactId, message);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -998,9 +1198,16 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		Offer o = new Offer(Arrays.asList(messageId, messageId1, messageId2,
-				messageId3));
-		db.receiveOffer(contactId, o);
+		Transaction transaction = db.startTransaction();
+		try {
+			Offer o = new Offer(Arrays.asList(messageId, messageId1,
+					messageId2, messageId3));
+			db.receiveOffer(transaction, contactId, o);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
+
 		context.assertIsSatisfied();
 	}
 
@@ -1026,8 +1233,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.receiveRequest(contactId, new Request(Collections.singletonList(
-				messageId)));
+		Transaction transaction = db.startTransaction();
+		try {
+			Request r = new Request(Collections.singletonList(messageId));
+			db.receiveRequest(transaction, contactId, r);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -1056,7 +1269,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.setVisibleToContact(contactId, groupId, true);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.setVisibleToContact(transaction, contactId, groupId, true);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -1083,45 +1302,56 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.setVisibleToContact(contactId, groupId, true);
+		Transaction transaction = db.startTransaction();
+		try {
+			db.setVisibleToContact(transaction, contactId, groupId, true);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testTransportKeys() throws Exception {
-		final TransportKeys keys = createTransportKeys();
+		final TransportKeys transportKeys = createTransportKeys();
+		final Map<ContactId, TransportKeys> keys = Collections.singletonMap(
+				contactId, transportKeys);
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			// updateTransportKeys()
+			// startTransaction()
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			// updateTransportKeys()
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			oneOf(database).containsTransport(txn, transportId);
 			will(returnValue(true));
-			oneOf(database).updateTransportKeys(txn,
-					Collections.singletonMap(contactId, keys));
-			oneOf(database).commitTransaction(txn);
+			oneOf(database).updateTransportKeys(txn, keys);
 			// getTransportKeys()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
 			oneOf(database).containsTransport(txn, transportId);
 			will(returnValue(true));
 			oneOf(database).getTransportKeys(txn, transportId);
-			will(returnValue(Collections.singletonMap(contactId, keys)));
+			will(returnValue(keys));
+			// endTransaction()
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.updateTransportKeys(Collections.singletonMap(contactId, keys));
-		assertEquals(Collections.singletonMap(contactId, keys),
-				db.getTransportKeys(transportId));
+		Transaction transaction = db.startTransaction();
+		try {
+			db.updateTransportKeys(transaction, keys);
+			assertEquals(keys, db.getTransportKeys(transaction, transportId));
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -1162,29 +1392,34 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			// mergeSettings()
+			// startTransaction()
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			// mergeSettings()
 			oneOf(database).getSettings(txn, "namespace");
 			will(returnValue(before));
 			oneOf(database).mergeSettings(txn, update, "namespace");
-			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(SettingsUpdatedEvent.class)));
 			// mergeSettings() again
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
 			oneOf(database).getSettings(txn, "namespace");
 			will(returnValue(merged));
+			// endTransaction()
 			oneOf(database).commitTransaction(txn);
 		}});
 
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		// First merge should broadcast an event
-		db.mergeSettings(update, "namespace");
-		// Second merge should not broadcast an event
-		db.mergeSettings(update, "namespace");
+		Transaction transaction = db.startTransaction();
+		try {
+			// First merge should broadcast an event
+			db.mergeSettings(transaction, update, "namespace");
+			// Second merge should not broadcast an event
+			db.mergeSettings(transaction, update, "namespace");
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
diff --git a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
index 23fbddea8dc420ac9afa29677a0454b6c8ec8edf..25cb91d0352c0f7e3d372d47861436696f7f4626 100644
--- a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
@@ -3,6 +3,7 @@ package org.briarproject.plugins;
 import org.briarproject.BriarTestCase;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
@@ -14,6 +15,7 @@ import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
 import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
 import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
 import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.settings.SettingsManager;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.ui.UiCallback;
 import org.briarproject.system.SystemClock;
@@ -46,6 +48,8 @@ public class PluginManagerImplTest extends BriarTestCase {
 		final Poller poller = context.mock(Poller.class);
 		final ConnectionManager connectionManager =
 				context.mock(ConnectionManager.class);
+		final SettingsManager settingsManager =
+				context.mock(SettingsManager.class);
 		final TransportPropertyManager transportPropertyManager =
 				context.mock(TransportPropertyManager.class);
 		final UiCallback uiCallback = context.mock(UiCallback.class);
@@ -55,18 +59,21 @@ public class PluginManagerImplTest extends BriarTestCase {
 		final SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
 		final TransportId simplexId = new TransportId("simplex");
 		final int simplexLatency = 12345;
+		final Transaction simplexTxn = new Transaction(null);
 		final SimplexPluginFactory simplexFailFactory =
 				context.mock(SimplexPluginFactory.class, "simplexFailFactory");
 		final SimplexPlugin simplexFailPlugin =
 				context.mock(SimplexPlugin.class, "simplexFailPlugin");
 		final TransportId simplexFailId = new TransportId("simplex1");
 		final int simplexFailLatency = 23456;
+		final Transaction simplexFailTxn = new Transaction(null);
 		// Two duplex plugin factories: one creates a plugin, the other fails
 		final DuplexPluginFactory duplexFactory =
 				context.mock(DuplexPluginFactory.class);
 		final DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class);
 		final TransportId duplexId = new TransportId("duplex");
 		final int duplexLatency = 34567;
+		final Transaction duplexTxn = new Transaction(null);
 		final DuplexPluginFactory duplexFailFactory =
 				context.mock(DuplexPluginFactory.class, "duplexFailFactory");
 		final TransportId duplexFailId = new TransportId("duplex1");
@@ -82,7 +89,10 @@ public class PluginManagerImplTest extends BriarTestCase {
 			will(returnValue(simplexPlugin)); // Created
 			oneOf(simplexPlugin).getMaxLatency();
 			will(returnValue(simplexLatency));
-			oneOf(db).addTransport(simplexId, simplexLatency);
+			oneOf(db).startTransaction();
+			will(returnValue(simplexTxn));
+			oneOf(db).addTransport(simplexTxn, simplexId, simplexLatency);
+			oneOf(db).endTransaction(simplexTxn);
 			oneOf(simplexPlugin).start();
 			will(returnValue(true)); // Started
 			oneOf(simplexPlugin).shouldPoll();
@@ -96,7 +106,11 @@ public class PluginManagerImplTest extends BriarTestCase {
 			will(returnValue(simplexFailPlugin)); // Created
 			oneOf(simplexFailPlugin).getMaxLatency();
 			will(returnValue(simplexFailLatency));
-			oneOf(db).addTransport(simplexFailId, simplexFailLatency);
+			oneOf(db).startTransaction();
+			will(returnValue(simplexFailTxn));
+			oneOf(db).addTransport(simplexFailTxn, simplexFailId,
+					simplexFailLatency);
+			oneOf(db).endTransaction(simplexFailTxn);
 			oneOf(simplexFailPlugin).start();
 			will(returnValue(false)); // Failed to start
 			// First duplex plugin
@@ -109,7 +123,10 @@ public class PluginManagerImplTest extends BriarTestCase {
 			will(returnValue(duplexPlugin)); // Created
 			oneOf(duplexPlugin).getMaxLatency();
 			will(returnValue(duplexLatency));
-			oneOf(db).addTransport(duplexId, duplexLatency);
+			oneOf(db).startTransaction();
+			will(returnValue(duplexTxn));
+			oneOf(db).addTransport(duplexTxn, duplexId, duplexLatency);
+			oneOf(db).endTransaction(duplexTxn);
 			oneOf(duplexPlugin).start();
 			will(returnValue(true)); // Started
 			oneOf(duplexPlugin).shouldPoll();
@@ -128,7 +145,8 @@ public class PluginManagerImplTest extends BriarTestCase {
 		}});
 		PluginManagerImpl p = new PluginManagerImpl(ioExecutor, eventBus,
 				simplexPluginConfig, duplexPluginConfig, clock, db, poller,
-				connectionManager, transportPropertyManager, uiCallback);
+				connectionManager, settingsManager, transportPropertyManager,
+				uiCallback);
 
 		// Two plugins should be started and stopped
 		assertTrue(p.start());
diff --git a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
index a4bd5d20e44c644504e3449fd919e72c54ab8c99..bfb815c2d52c1d0a4042278f1d4f641a84423f80 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
@@ -13,6 +13,7 @@ import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.StorageStatus;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
@@ -120,7 +121,13 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		lifecycleManager.startServices();
 		lifecycleManager.waitForStartup();
 		// Add a transport
-		db.addTransport(transportId, MAX_LATENCY);
+		Transaction txn = db.startTransaction();
+		try {
+			db.addTransport(txn, transportId, MAX_LATENCY);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		// Add an identity for Alice
 		LocalAuthor aliceAuthor = new LocalAuthor(aliceId, "Alice",
 				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp,
@@ -185,7 +192,13 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		lifecycleManager.startServices();
 		lifecycleManager.waitForStartup();
 		// Add a transport
-		db.addTransport(transportId, MAX_LATENCY);
+		Transaction txn = db.startTransaction();
+		try {
+			db.addTransport(txn, transportId, MAX_LATENCY);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		// Add an identity for Bob
 		LocalAuthor bobAuthor = new LocalAuthor(bobId, "Bob",
 				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp,
diff --git a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
index 7b49108ef5df3c7904d6390872d8ad0f8b7b63d8..635b2b0685934562a494303500850b0900ecd98a 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
@@ -5,6 +5,7 @@ import org.briarproject.TestUtils;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.MessageId;
@@ -49,16 +50,24 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 		final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
 				dbExecutor, eventBus, contactId, transportId, maxLatency,
 				packetWriter);
+		final Transaction noAckTxn = new Transaction(null);
+		final Transaction noMsgTxn = new Transaction(null);
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
 			// No acks to send
-			oneOf(db).generateAck(contactId, MAX_MESSAGE_IDS);
+			oneOf(db).startTransaction();
+			will(returnValue(noAckTxn));
+			oneOf(db).generateAck(noAckTxn, contactId, MAX_MESSAGE_IDS);
 			will(returnValue(null));
+			oneOf(db).endTransaction(noAckTxn);
 			// No messages to send
-			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(maxLatency));
+			oneOf(db).startTransaction();
+			will(returnValue(noMsgTxn));
+			oneOf(db).generateBatch(with(noMsgTxn), with(contactId),
+					with(any(int.class)), with(maxLatency));
 			will(returnValue(null));
+			oneOf(db).endTransaction(noMsgTxn);
 			// Flush the output stream
 			oneOf(packetWriter).flush();
 			// Remove listener
@@ -75,25 +84,41 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 		final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
 				dbExecutor, eventBus, contactId, transportId, maxLatency,
 				packetWriter);
+		final Transaction ackTxn = new Transaction(null);
+		final Transaction noAckTxn = new Transaction(null);
+		final Transaction msgTxn = new Transaction(null);
+		final Transaction noMsgTxn = new Transaction(null);
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
 			// One ack to send
-			oneOf(db).generateAck(contactId, MAX_MESSAGE_IDS);
+			oneOf(db).startTransaction();
+			will(returnValue(ackTxn));
+			oneOf(db).generateAck(ackTxn, contactId, MAX_MESSAGE_IDS);
 			will(returnValue(ack));
+			oneOf(db).endTransaction(ackTxn);
 			oneOf(packetWriter).writeAck(ack);
-			// No more acks
-			oneOf(db).generateAck(contactId, MAX_MESSAGE_IDS);
-			will(returnValue(null));
 			// One message to send
-			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(maxLatency));
+			oneOf(db).startTransaction();
+			will(returnValue(msgTxn));
+			oneOf(db).generateBatch(with(msgTxn), with(contactId),
+					with(any(int.class)), with(maxLatency));
 			will(returnValue(Arrays.asList(raw)));
+			oneOf(db).endTransaction(msgTxn);
 			oneOf(packetWriter).writeMessage(raw);
+			// No more acks
+			oneOf(db).startTransaction();
+			will(returnValue(noAckTxn));
+			oneOf(db).generateAck(noAckTxn, contactId, MAX_MESSAGE_IDS);
+			will(returnValue(null));
+			oneOf(db).endTransaction(noAckTxn);
 			// No more messages
-			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(maxLatency));
+			oneOf(db).startTransaction();
+			will(returnValue(noMsgTxn));
+			oneOf(db).generateBatch(with(noMsgTxn), with(contactId),
+					with(any(int.class)), with(maxLatency));
 			will(returnValue(null));
+			oneOf(db).endTransaction(noMsgTxn);
 			// Flush the output stream
 			oneOf(packetWriter).flush();
 			// Remove listener