diff --git a/briar-api/src/org/briarproject/api/contact/Contact.java b/briar-api/src/org/briarproject/api/contact/Contact.java
index 18a2cb446c31334407bfca08f916f6a94eea2d65..5c5b9b197e8ea868595c67d622d0387fab25fa77 100644
--- a/briar-api/src/org/briarproject/api/contact/Contact.java
+++ b/briar-api/src/org/briarproject/api/contact/Contact.java
@@ -1,6 +1,5 @@
 package org.briarproject.api.contact;
 
-import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 
@@ -9,14 +8,11 @@ public class Contact {
 	private final ContactId id;
 	private final Author author;
 	private final AuthorId localAuthorId;
-	private final StorageStatus status;
 
-	public Contact(ContactId id, Author author, AuthorId localAuthorId,
-			StorageStatus status) {
+	public Contact(ContactId id, Author author, AuthorId localAuthorId) {
 		this.id = id;
 		this.author = author;
 		this.localAuthorId = localAuthorId;
-		this.status = status;
 	}
 
 	public ContactId getId() {
@@ -31,10 +27,6 @@ public class Contact {
 		return localAuthorId;
 	}
 
-	public StorageStatus getStatus() {
-		return status;
-	}
-
 	@Override
 	public int hashCode() {
 		return id.hashCode();
diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java
index 08c9dc46a1509fa5efca37571502e49898af6830..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(ContactId c);
+		void addingContact(Transaction txn, Contact c) throws DbException;
 	}
 
 	interface RemoveContactHook {
-		void removingContact(ContactId 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 5609844f42a6c8337dbdc0b30d07b894c2b93773..ca24976942a3d6b7dbdaa160d6825e19a32fbfa6 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -26,57 +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;
+
+	/**
+	 * Ends a transaction. If the transaction is marked as complete, the
+	 * transaction is committed and any events attached to the transaction are
+	 * broadcast; otherwise the transaction is aborted.
+	 */
+	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
@@ -84,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
@@ -108,168 +137,214 @@ 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 transport keys for the given transport. */
-	Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
-			throws DbException;
+	/**
+	 * Returns all settings in the given namespace.
+	 */
+	Settings getSettings(Transaction txn, String namespace) throws DbException;
 
-	/** Returns the maximum latencies in milliseconds of all transports. */
-	Map<TransportId, Integer> getTransportLatencies() throws DbException;
+	/**
+	 * Returns all transport keys for the given transport.
+	 */
+	Map<ContactId, TransportKeys> getTransportKeys(Transaction txn,
+			TransportId t) throws DbException;
 
-	/** Returns the IDs of all contacts to which the given group is visible. */
-	Collection<ContactId> getVisibility(GroupId g) 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;
-
-	/** Removes a transport (and all associated state) from the database. */
-	void removeTransport(TransportId t) throws DbException;
+	void removeLocalAuthor(Transaction txn, AuthorId a) throws DbException;
 
-	/** Sets the status of the given contact. */
-	void setContactStatus(ContactId c, StorageStatus s) throws DbException;
-
-	/** Sets the status of the given local pseudonym. */
-	void setLocalAuthorStatus(AuthorId a, StorageStatus s)
-		throws DbException;
+	/**
+	 * Removes a transport (and all associated state) from the database.
+	 */
+	void removeTransport(Transaction txn, TransportId t) 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 to the given set of contacts and invisible to any
-	 * other contacts.
+	 * Makes a group visible or invisible to a contact.
 	 */
-	void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException;
-
-	/** Makes a group visible or invisible to a contact. */
-	void setVisibleToContact(ContactId c, GroupId g, boolean visible)
-			throws DbException;
+	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/StorageStatus.java b/briar-api/src/org/briarproject/api/db/StorageStatus.java
deleted file mode 100644
index dc2d554ffb08863f6409067ca727bc169aad05e0..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/db/StorageStatus.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.briarproject.api.db;
-
-public enum StorageStatus {
-
-	ADDING(0), ACTIVE(1), REMOVING(2);
-
-	private final int value;
-
-	StorageStatus(int value) {
-		this.value = value;
-	}
-
-	public int getValue() {
-		return value;
-	}
-
-	public static StorageStatus fromValue(int value) {
-		for (StorageStatus s : values()) if (s.value == value) return s;
-		throw new IllegalArgumentException();
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/db/Transaction.java b/briar-api/src/org/briarproject/api/db/Transaction.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb89f142ca3461582180f53f9ffb9e557e0cdc64
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/db/Transaction.java
@@ -0,0 +1,63 @@
+package org.briarproject.api.db;
+
+import org.briarproject.api.event.Event;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A wrapper around a database transaction. Transactions are not thread-safe.
+ */
+public class Transaction {
+
+	private final Object txn;
+
+	private List<Event> events = null;
+	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;
+	}
+
+	/**
+	 * Attaches an event to be broadcast when the transaction has been
+	 * committed.
+	 */
+	public void attach(Event e) {
+		if (events == null) events = new ArrayList<Event>();
+		events.add(e);
+	}
+
+	/**
+	 * Returns any events attached to the transaction.
+	 */
+	public List<Event> getEvents() {
+		if (events == null) return Collections.emptyList();
+		return events;
+	}
+
+	/**
+	 * 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 9a5a030abdeab1d485ee7a22fd6f8d3a78ac1ea5..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(AuthorId a);
+		void addingIdentity(Transaction txn, LocalAuthor a) throws DbException;
 	}
 
 	interface RemoveIdentityHook {
-		void removingIdentity(AuthorId a);
+		void removingIdentity(Transaction txn, LocalAuthor a)
+				throws DbException;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/identity/LocalAuthor.java b/briar-api/src/org/briarproject/api/identity/LocalAuthor.java
index 08ee121e533a7fb666705d3583032a7029390f43..cdcfdc788a4932c62625935f13fbcd1eebecd72c 100644
--- a/briar-api/src/org/briarproject/api/identity/LocalAuthor.java
+++ b/briar-api/src/org/briarproject/api/identity/LocalAuthor.java
@@ -1,20 +1,16 @@
 package org.briarproject.api.identity;
 
-import org.briarproject.api.db.StorageStatus;
-
 /** A pseudonym for the local user. */
 public class LocalAuthor extends Author {
 
 	private final byte[] privateKey;
 	private final long created;
-	private final StorageStatus status;
 
 	public LocalAuthor(AuthorId id, String name, byte[] publicKey,
-			byte[] privateKey, long created, StorageStatus status) {
+			byte[] privateKey, long created) {
 		super(id, name, publicKey);
 		this.privateKey = privateKey;
 		this.created = created;
-		this.status = status;
 	}
 
 	/**  Returns the private key used to generate the pseudonym's signatures. */
@@ -29,9 +25,4 @@ public class LocalAuthor extends Author {
 	public long getTimeCreated() {
 		return created;
 	}
-
-	/** Returns the status of the pseudonym. */
-	public StorageStatus getStatus() {
-		return status;
-	}
 }
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 fa1c096219f35df6889f892839273b8806af4b94..2fafe8a168c4c22bdbb97f1d6143cf4ef520d24f 100644
--- a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
+++ b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
@@ -7,75 +7,29 @@ import org.briarproject.api.contact.ContactId;
 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.event.ContactAddedEvent;
-import org.briarproject.api.event.ContactRemovedEvent;
-import org.briarproject.api.event.EventBus;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager.RemoveIdentityHook;
-import org.briarproject.api.lifecycle.Service;
+import org.briarproject.api.identity.LocalAuthor;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.logging.Logger;
 
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.db.StorageStatus.ACTIVE;
-import static org.briarproject.api.db.StorageStatus.ADDING;
-import static org.briarproject.api.db.StorageStatus.REMOVING;
-
-class ContactManagerImpl implements ContactManager, Service,
-		RemoveIdentityHook {
-
-	private static final Logger LOG =
-			Logger.getLogger(ContactManagerImpl.class.getName());
+class ContactManagerImpl implements ContactManager, RemoveIdentityHook {
 
 	private final DatabaseComponent db;
-	private final EventBus eventBus;
 	private final List<AddContactHook> addHooks;
 	private final List<RemoveContactHook> removeHooks;
 
 	@Inject
-	ContactManagerImpl(DatabaseComponent db, EventBus eventBus) {
+	ContactManagerImpl(DatabaseComponent db) {
 		this.db = db;
-		this.eventBus = eventBus;
 		addHooks = new CopyOnWriteArrayList<AddContactHook>();
 		removeHooks = new CopyOnWriteArrayList<RemoveContactHook>();
 	}
 
-	@Override
-	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.getId());
-					db.setContactStatus(c.getId(), ACTIVE);
-					eventBus.broadcast(new ContactAddedEvent(c.getId()));
-				} else if (c.getStatus().equals(REMOVING)) {
-					for (RemoveContactHook hook : removeHooks)
-						hook.removingContact(c.getId());
-					db.removeContact(c.getId());
-					eventBus.broadcast(new ContactRemovedEvent(c.getId()));
-				}
-			}
-			return true;
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			return false;
-		}
-	}
-
-	@Override
-	public boolean stop() {
-		return true;
-	}
-
 	@Override
 	public void registerAddContactHook(AddContactHook hook) {
 		addHooks.add(hook);
@@ -89,45 +43,70 @@ class ContactManagerImpl implements ContactManager, Service,
 	@Override
 	public ContactId addContact(Author remote, AuthorId local)
 			throws DbException {
-		ContactId c = db.addContact(remote, local);
-		for (AddContactHook hook : addHooks) hook.addingContact(c);
-		db.setContactStatus(c, ACTIVE);
-		eventBus.broadcast(new ContactAddedEvent(c));
+		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);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 		return c;
 	}
 
 	@Override
 	public Contact getContact(ContactId c) throws DbException {
-		Contact contact = db.getContact(c);
-		if (contact.getStatus().equals(ACTIVE)) return contact;
-		throw new NoSuchContactException();
+		Contact contact;
+		Transaction txn = db.startTransaction();
+		try {
+			contact = db.getContact(txn, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return contact;
 	}
 
 	@Override
 	public Collection<Contact> getContacts() throws DbException {
-		Collection<Contact> contacts = db.getContacts();
-		// Filter out any contacts that are being added or removed
-		List<Contact> active = new ArrayList<Contact>(contacts.size());
-		for (Contact c : contacts)
-			if (c.getStatus().equals(ACTIVE)) active.add(c);
-		return Collections.unmodifiableList(active);
+		Collection<Contact> contacts;
+		Transaction txn = db.startTransaction();
+		try {
+			contacts = db.getContacts(txn);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return contacts;
 	}
 
 	@Override
 	public void removeContact(ContactId c) throws DbException {
-		db.setContactStatus(c, REMOVING);
-		for (RemoveContactHook hook : removeHooks) hook.removingContact(c);
-		db.removeContact(c);
-		eventBus.broadcast(new ContactRemovedEvent(c));
+		Transaction txn = db.startTransaction();
+		try {
+			removeContact(txn, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	private void removeContact(Transaction txn, ContactId c)
+			throws DbException {
+		Contact contact = db.getContact(txn, c);
+		for (RemoveContactHook hook : removeHooks)
+			hook.removingContact(txn, contact);
+		db.removeContact(txn, c);
 	}
 
 	@Override
-	public void removingIdentity(AuthorId 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)) 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/contact/ContactModule.java b/briar-core/src/org/briarproject/contact/ContactModule.java
index 5bfb8e404e0748b33a6095c3a5c263f6a3c2086b..8e9f3d632bb66dd64cf28b5e156d736c9bf9c43b 100644
--- a/briar-core/src/org/briarproject/contact/ContactModule.java
+++ b/briar-core/src/org/briarproject/contact/ContactModule.java
@@ -5,7 +5,6 @@ import com.google.inject.Provides;
 
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.identity.IdentityManager;
-import org.briarproject.api.lifecycle.LifecycleManager;
 
 import javax.inject.Singleton;
 
@@ -15,10 +14,8 @@ public class ContactModule extends AbstractModule {
 	protected void configure() {}
 
 	@Provides @Singleton
-	ContactManager getContactManager(LifecycleManager lifecycleManager,
-			IdentityManager identityManager,
+	ContactManager getContactManager(IdentityManager identityManager,
 			ContactManagerImpl contactManager) {
-		lifecycleManager.register(contactManager);
 		identityManager.registerRemoveIdentityHook(contactManager);
 		return contactManager;
 	}
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index 934156308b6b53ef99322206b16f0f642ce41bad..91d17b49df5abd4f1746afb17f62f83f954b659c 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -6,7 +6,6 @@ import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
-import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
@@ -30,27 +29,23 @@ import java.util.Map;
  * obtained by calling {@link #startTransaction()}. Every transaction must be
  * terminated by calling either {@link #abortTransaction(T)} or
  * {@link #commitTransaction(T)}, even if an exception is thrown.
- * <p>
- * Read-write locking is provided by the DatabaseComponent implementation.
  */
 interface Database<T> {
 
 	/**
 	 * Opens the database and returns true if the database already existed.
-	 * <p>
-	 * Locking: write.
 	 */
 	boolean open() throws DbException;
 
 	/**
 	 * Prevents new transactions from starting, waits for all current
 	 * transactions to finish, and closes the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void close() throws DbException, IOException;
 
-	/** Starts a new transaction and returns an object representing it. */
+	/**
+	 * Starts a new transaction and returns an object representing it.
+	 */
 	T startTransaction() throws DbException;
 
 	/**
@@ -65,59 +60,39 @@ interface Database<T> {
 	 */
 	void commitTransaction(T txn) throws DbException;
 
-	/**
-	 * Returns the number of transactions started since the transaction count
-	 * was last reset.
-	 */
-	int getTransactionCount();
-
-	/**  Resets the transaction count. */
-	void resetTransactionCount();
-
 	/**
 	 * Stores a contact associated with the given local and remote pseudonyms,
 	 * and returns an ID for the contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	ContactId addContact(T txn, Author remote, AuthorId local)
 			throws DbException;
 
 	/**
 	 * Stores a group.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addGroup(T txn, Group g) throws DbException;
 
 	/**
 	 * Stores a local pseudonym.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addLocalAuthor(T txn, LocalAuthor a) throws DbException;
 
 	/**
 	 * Stores a message.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addMessage(T txn, Message m, Validity validity, boolean shared)
 			throws DbException;
 
 	/**
 	 * Records that a message has been offered by the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
 
 	/**
 	 * Initialises the status of the given message with respect to the given
 	 * contact.
-	 * <p>
-	 * Locking: write.
-	 * @param ack whether the message needs to be acknowledged.
+	 *
+	 * @param ack  whether the message needs to be acknowledged.
 	 * @param seen whether the contact has seen the message.
 	 */
 	void addStatus(T txn, ContactId c, MessageId m, boolean ack, boolean seen)
@@ -125,76 +100,56 @@ interface Database<T> {
 
 	/**
 	 * Stores a transport.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addTransport(T txn, TransportId t, int maxLatency)
 			throws DbException;
 
 	/**
 	 * Stores transport keys for a newly added contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addTransportKeys(T txn, ContactId c, TransportKeys k)
 			throws DbException;
 
 	/**
 	 * Makes a group visible to the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void addVisibility(T txn, ContactId c, GroupId g) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given contact for the given
 	 * local pseudonym.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsContact(T txn, AuthorId remote, AuthorId local)
 			throws DbException;
 
 	/**
 	 * Returns true if the database contains the given contact.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsContact(T txn, ContactId c) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given group.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsGroup(T txn, GroupId g) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given local pseudonym.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsLocalAuthor(T txn, AuthorId a) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given message.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsMessage(T txn, MessageId m) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given transport.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsTransport(T txn, TransportId t) throws DbException;
 
 	/**
 	 * Returns true if the database contains the given group and the group is
 	 * visible to the given contact.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsVisibleGroup(T txn, ContactId c, GroupId g)
 			throws DbException;
@@ -202,16 +157,12 @@ interface Database<T> {
 	/**
 	 * Returns true if the database contains the given message and the message
 	 * is visible to the given contact.
-	 * <p>
-	 * Locking: read.
 	 */
 	boolean containsVisibleMessage(T txn, ContactId c, MessageId m)
 			throws DbException;
 
 	/**
 	 * Returns the number of messages offered by the given contact.
-	 * <p>
-	 * Locking: read.
 	 */
 	int countOfferedMessages(T txn, ContactId c) throws DbException;
 
@@ -234,36 +185,26 @@ interface Database<T> {
 
 	/**
 	 * Returns the contact with the given ID.
-	 * <p>
-	 * Locking: read.
 	 */
 	Contact getContact(T txn, ContactId c) throws DbException;
 
 	/**
 	 * Returns the IDs of all contacts.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<ContactId> getContactIds(T txn) throws DbException;
 
 	/**
 	 * Returns all contacts.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<Contact> getContacts(T txn) throws DbException;
 
 	/**
 	 * Returns all contacts associated with the given local pseudonym.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<ContactId> getContacts(T txn, AuthorId a) throws DbException;
 
 	/**
 	 * Returns the unique ID for this device.
-	 * <p>
-	 * Locking: read.
 	 */
 	DeviceId getDeviceId(T txn) throws DbException;
 
@@ -276,59 +217,43 @@ interface Database<T> {
 
 	/**
 	 * Returns the group with the given ID.
-	 * <p>
-	 * Locking: read.
 	 */
 	Group getGroup(T txn, GroupId g) throws DbException;
 
 	/**
 	 * Returns the metadata for the given group.
-	 * <p>
-	 * Locking: read.
 	 */
 	Metadata getGroupMetadata(T txn, GroupId g) throws DbException;
 
 	/**
 	 * Returns all groups belonging to the given client.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<Group> getGroups(T txn, ClientId c) throws DbException;
 
 	/**
 	 * Returns the local pseudonym with the given ID.
-	 * <p>
-	 * Locking: read.
 	 */
 	LocalAuthor getLocalAuthor(T txn, AuthorId a) throws DbException;
 
 	/**
 	 * Returns all local pseudonyms.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<LocalAuthor> getLocalAuthors(T txn) throws DbException;
 
 	/**
 	 * Returns the metadata for all messages in the given group.
-	 * <p>
-	 * Locking: read.
 	 */
 	Map<MessageId, Metadata> getMessageMetadata(T txn, GroupId g)
 			throws DbException;
 
 	/**
 	 * Returns the metadata for the given message.
-	 * <p>
-	 * Locking: read.
 	 */
 	Metadata getMessageMetadata(T txn, MessageId m) throws DbException;
 
 	/**
 	 * Returns the status of all messages in the given group with respect
 	 * to the given contact.
-	 * <p>
-	 * Locking: read
 	 */
 	Collection<MessageStatus> getMessageStatus(T txn, ContactId c, GroupId g)
 			throws DbException;
@@ -336,8 +261,6 @@ interface Database<T> {
 	/**
 	 * Returns the status of the given message with respect to the given
 	 * contact.
-	 * <p>
-	 * Locking: read
 	 */
 	MessageStatus getMessageStatus(T txn, ContactId c, MessageId m)
 			throws DbException;
@@ -345,8 +268,6 @@ interface Database<T> {
 	/**
 	 * Returns the IDs of some messages received from the given contact that
 	 * need to be acknowledged, up to the given number of messages.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getMessagesToAck(T txn, ContactId c, int maxMessages)
 			throws DbException;
@@ -354,8 +275,6 @@ interface Database<T> {
 	/**
 	 * Returns the IDs of some messages that are eligible to be offered to the
 	 * given contact, up to the given number of messages.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getMessagesToOffer(T txn, ContactId c,
 			int maxMessages) throws DbException;
@@ -363,8 +282,6 @@ interface Database<T> {
 	/**
 	 * Returns the IDs of some messages that are eligible to be sent to the
 	 * given contact, up to the given total length.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getMessagesToSend(T txn, ContactId c, int maxLength)
 			throws DbException;
@@ -372,8 +289,6 @@ interface Database<T> {
 	/**
 	 * Returns the IDs of some messages that are eligible to be requested from
 	 * the given contact, up to the given number of messages.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getMessagesToRequest(T txn, ContactId c,
 			int maxMessages) throws DbException;
@@ -381,16 +296,12 @@ interface Database<T> {
 	/**
 	 * Returns the IDs of any messages that need to be validated by the given
 	 * client.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
 			throws DbException;
 
 	/**
 	 * Returns the message with the given ID, in serialised form.
-	 * <p>
-	 * Locking: read.
 	 */
 	byte[] getRawMessage(T txn, MessageId m) throws DbException;
 
@@ -398,46 +309,34 @@ interface Database<T> {
 	 * Returns the IDs of some messages that are eligible to be sent to the
 	 * given contact and have been requested by the contact, up to the given
 	 * total length.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<MessageId> getRequestedMessagesToSend(T txn, ContactId c,
 			int maxLength) throws DbException;
 
 	/**
 	 * Returns all settings in the given namespace.
-	 * <p>
-	 * Locking: read.
 	 */
 	Settings getSettings(T txn, String namespace) throws DbException;
 
 	/**
 	 * Returns all transport keys for the given transport.
-	 * <p>
-	 * Locking: read.
 	 */
 	Map<ContactId, TransportKeys> getTransportKeys(T txn, TransportId t)
 			throws DbException;
 
 	/**
 	 * Returns the maximum latencies in milliseconds of all transports.
-	 * <p>
-	 * Locking: read.
 	 */
 	Map<TransportId, Integer> getTransportLatencies(T txn) throws DbException;
 
 	/**
 	 * Returns the IDs of all contacts to which the given group is visible.
-	 * <p>
-	 * Locking: read.
 	 */
 	Collection<ContactId> getVisibility(T txn, GroupId g) throws DbException;
 
 	/**
 	 * Increments the outgoing stream counter for the given contact and
 	 * transport in the given rotation period.
-	 * <p>
-	 * Locking: write.
 	 */
 	void incrementStreamCounter(T txn, ContactId c, TransportId t,
 			long rotationPeriod) throws DbException;
@@ -445,8 +344,6 @@ interface Database<T> {
 	/**
 	 * Marks the given messages as not needing to be acknowledged to the
 	 * given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void lowerAckFlag(T txn, ContactId c, Collection<MessageId> acked)
 			throws DbException;
@@ -454,8 +351,6 @@ interface Database<T> {
 	/**
 	 * Marks the given messages as not having been requested by the given
 	 * contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void lowerRequestedFlag(T txn, ContactId c, Collection<MessageId> requested)
 			throws DbException;
@@ -463,8 +358,6 @@ interface Database<T> {
 	/*
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
-	 * <p>
-	 * Locking: write.
 	 */
 	void mergeGroupMetadata(T txn, GroupId g, Metadata meta)
 			throws DbException;
@@ -472,8 +365,6 @@ interface Database<T> {
 	/*
 	 * Merges the given metadata with the existing metadata for the given
 	 * message.
-	 * <p>
-	 * Locking: write.
 	 */
 	void mergeMessageMetadata(T txn, MessageId m, Metadata meta)
 			throws DbException;
@@ -481,65 +372,47 @@ interface Database<T> {
 	/**
 	 * Merges the given settings with the existing settings in the given
 	 * namespace.
-	 * <p>
-	 * Locking: write.
 	 */
 	void mergeSettings(T txn, Settings s, String namespace) throws DbException;
 
 	/**
 	 * Marks a message as needing to be acknowledged to the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void raiseAckFlag(T txn, ContactId c, MessageId m) throws DbException;
 
 	/**
 	 * Marks a message as having been requested by the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void raiseRequestedFlag(T txn, ContactId c, MessageId m) throws DbException;
 
 	/**
 	 * Marks a message as having been seen by the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
 
 	/**
 	 * Removes a contact from the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
 	/**
 	 * Removes a group (and all associated state) from the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeGroup(T txn, GroupId g) throws DbException;
 
 	/**
 	 * Removes a local pseudonym (and all associated state) from the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeLocalAuthor(T txn, AuthorId a) throws DbException;
 
 	/**
 	 * Removes a message (and all associated state) from the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeMessage(T txn, MessageId m) throws DbException;
 
 	/**
 	 * Removes an offered message that was offered by the given contact, or
 	 * returns false if there is no such message.
-	 * <p>
-	 * Locking: write.
 	 */
 	boolean removeOfferedMessage(T txn, ContactId c, MessageId m)
 			throws DbException;
@@ -547,70 +420,40 @@ interface Database<T> {
 	/**
 	 * Removes the given offered messages that were offered by the given
 	 * contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeOfferedMessages(T txn, ContactId c,
 			Collection<MessageId> requested) throws DbException;
 
 	/**
 	 * Removes a transport (and all associated state) from the database.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeTransport(T txn, TransportId t) throws DbException;
 
 	/**
 	 * Makes a group invisible to the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void removeVisibility(T txn, ContactId c, GroupId g) throws DbException;
 
 	/**
 	 * Resets the transmission count and expiry time of the given message with
 	 * respect to the given contact.
-	 * <p>
-	 * Locking: write.
 	 */
 	void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
 
-	/**
-	 * Sets the status of the given contact.
-	 * <p>
-	 * Locking: write.
-	 */
-	void setContactStatus(T txn, ContactId c, StorageStatus s)
-			throws DbException;
-
-	/**
-	 * Sets the status of the given local pseudonym.
-	 * <p>
-	 * Locking: write.
-	 */
-	void setLocalAuthorStatus(T txn, AuthorId a, StorageStatus s)
-			throws DbException;
-
 	/**
 	 * Marks the given message as shared or unshared.
-	 * <p>
-	 * Locking: write.
 	 */
 	void setMessageShared(T txn, MessageId m, boolean shared)
 			throws DbException;
 
 	/**
 	 * Marks the given message as valid or invalid.
-	 * <p>
-	 * Locking: write.
 	 */
 	void setMessageValid(T txn, MessageId m, boolean valid) throws DbException;
 
 	/**
 	 * Sets the reordering window for the given contact and transport in the
 	 * given rotation period.
-	 * <p>
-	 * Locking: write.
 	 */
 	void setReorderingWindow(T txn, ContactId c, TransportId t,
 			long rotationPeriod, long base, byte[] bitmap) throws DbException;
@@ -619,16 +462,12 @@ interface Database<T> {
 	 * Updates the transmission count and expiry time of the given message
 	 * with respect to the given contact, using the latency of the transport
 	 * over which it was sent.
-	 * <p>
-	 * Locking: write.
 	 */
 	void updateExpiryTime(T txn, ContactId c, MessageId m, int maxLatency)
 			throws DbException;
 
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
-	 * <p>
-	 * Locking: write.
 	 */
 	void updateTransportKeys(T txn, Map<ContactId, TransportKeys> keys)
 			throws DbException;
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 0a974ed8edb9095e56ea94fabdf635df4258f617..8ed1ece7857cbe02be91db7df2a8b13818d5dbcf 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -13,11 +13,16 @@ import org.briarproject.api.db.NoSuchGroupException;
 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.ContactAddedEvent;
+import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.GroupAddedEvent;
 import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
+import org.briarproject.api.event.LocalAuthorAddedEvent;
+import org.briarproject.api.event.LocalAuthorRemovedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
@@ -55,7 +60,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
@@ -77,19 +82,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			Logger.getLogger(DatabaseComponentImpl.class.getName());
 
 	private final Database<T> db;
+	private final Class<T> txnClass;
 	private final EventBus eventBus;
 	private final ShutdownManager shutdown;
+	private final AtomicBoolean closed = new AtomicBoolean(false);
 
-	private final ReentrantReadWriteLock lock =
-			new ReentrantReadWriteLock(true);
-
-	private boolean open = false; // Locking: lock.writeLock
-	private int shutdownHandle = -1; // Locking: lock.writeLock
+	private volatile int shutdownHandle = -1;
 
 	@Inject
-	DatabaseComponentImpl(Database<T> db, EventBus eventBus,
+	DatabaseComponentImpl(Database<T> db, Class<T> txnClass, EventBus eventBus,
 			ShutdownManager shutdown) {
 		this.db = db;
+		this.txnClass = txnClass;
 		this.eventBus = eventBus;
 		this.shutdown = shutdown;
 	}
@@ -97,9 +101,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	public boolean open() throws DbException {
 		Runnable shutdownHook = new Runnable() {
 			public void run() {
-				lock.writeLock().lock();
 				try {
-					shutdownHandle = -1;
 					close();
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -107,130 +109,85 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				} catch (IOException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
-				} finally {
-					lock.writeLock().unlock();
 				}
 			}
 		};
-		lock.writeLock().lock();
-		try {
-			if (open) throw new IllegalStateException();
-			open = true;
-			boolean reopened = db.open();
-			shutdownHandle = shutdown.addShutdownHook(shutdownHook);
-			return reopened;
-		} finally {
-			lock.writeLock().unlock();
-		}
+		boolean reopened = db.open();
+		shutdownHandle = shutdown.addShutdownHook(shutdownHook);
+		return reopened;
 	}
 
 	public void close() throws DbException, IOException {
-		lock.writeLock().lock();
-		try {
-			if (!open) return;
-			open = false;
-			if (shutdownHandle != -1)
-				shutdown.removeShutdownHook(shutdownHandle);
-			db.close();
-		} finally {
-			lock.writeLock().unlock();
-		}
+		if (closed.getAndSet(true)) return;
+		shutdown.removeShutdownHook(shutdownHandle);
+		db.close();
 	}
 
-	public ContactId addContact(Author remote, AuthorId local)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public Transaction startTransaction() throws DbException {
+		return new Transaction(db.startTransaction());
 	}
 
-	public void addGroup(Group g) throws DbException {
-		boolean added = false;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public void endTransaction(Transaction transaction) throws DbException {
+		T txn = txnClass.cast(transaction.unbox());
+		if (transaction.isComplete()) {
+			db.commitTransaction(txn);
+			for (Event e : transaction.getEvents()) eventBus.broadcast(e);
+		} else {
+			db.abortTransaction(txn);
 		}
-		if (added) eventBus.broadcast(new GroupAddedEvent(g));
 	}
 
-	public void addLocalAuthor(LocalAuthor a) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	private T unbox(Transaction transaction) {
+		if (transaction.isComplete()) throw new IllegalStateException();
+		return txnClass.cast(transaction.unbox());
+	}
+
+	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();
+		ContactId c = db.addContact(txn, remote, local);
+		transaction.attach(new ContactAddedEvent(c));
+		return c;
+	}
+
+	public void addGroup(Transaction transaction, Group g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsGroup(txn, g.getId())) {
+			db.addGroup(txn, g);
+			transaction.attach(new GroupAddedEvent(g));
 		}
 	}
 
-	public void addLocalMessage(Message m, ClientId c, Metadata meta,
-			boolean shared) throws DbException {
-		boolean added = false;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public void addLocalAuthor(Transaction transaction, LocalAuthor a)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a.getId())) {
+			db.addLocalAuthor(txn, a);
+			transaction.attach(new LocalAuthorAddedEvent(a.getId()));
 		}
-		if (added) {
-			eventBus.broadcast(new MessageAddedEvent(m, null));
-			eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
-			if (shared) eventBus.broadcast(new MessageSharedEvent(m));
+	}
+
+	public void addLocalMessage(Transaction transaction, Message m, ClientId c,
+			Metadata meta, boolean shared) throws DbException {
+		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);
+			transaction.attach(new MessageAddedEvent(m, null));
+			transaction.attach(new MessageValidatedEvent(m, c, true, true));
+			if (shared) transaction.attach(new MessageSharedEvent(m));
 		}
+		db.mergeMessageMetadata(txn, m.getId(), meta);
 	}
 
 	/**
 	 * Stores a message and initialises its status with respect to each contact.
-	 * <p>
-	 * Locking: write.
+	 *
 	 * @param sender null for a locally generated message.
 	 */
 	private void addMessage(T txn, Message m, Validity validity, boolean shared,
@@ -251,1053 +208,457 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void addTransport(TransportId t, int maxLatency) throws DbException {
-		boolean added = false;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (added) eventBus.broadcast(new TransportAddedEvent(t, maxLatency));
-	}
-
-	public void addTransportKeys(ContactId c, TransportKeys k)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public void addTransport(Transaction transaction, TransportId t,
+			int maxLatency) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsTransport(txn, t)) {
+			db.addTransport(txn, t, maxLatency);
+			transaction.attach(new TransportAddedEvent(t, maxLatency));
 		}
 	}
 
-	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 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 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 deleteMessage(Transaction transaction, MessageId m)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		db.deleteMessage(txn, m);
 	}
 
-	public Ack generateAck(ContactId c, int maxMessages) throws DbException {
-		Collection<MessageId> ids;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} 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(Transaction transaction, ContactId c,
+			int maxMessages) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		Collection<MessageId> ids = db.getMessagesToAck(txn, c, maxMessages);
 		if (ids.isEmpty()) return null;
+		db.lowerAckFlag(txn, c, ids);
 		return new Ack(ids);
 	}
 
-	public Collection<byte[]> generateBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException {
-		Collection<MessageId> ids;
-		List<byte[]> messages = new ArrayList<byte[]>();
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public Collection<byte[]> generateBatch(Transaction transaction,
+			ContactId c, int maxLength, int maxLatency) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		Collection<MessageId> ids = db.getMessagesToSend(txn, c, maxLength);
+		List<byte[]> messages = new ArrayList<byte[]>(ids.size());
+		for (MessageId m : ids) {
+			messages.add(db.getRawMessage(txn, m));
+			db.updateExpiryTime(txn, c, m, maxLatency);
 		}
-		if (messages.isEmpty()) return null;
-		if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
+		if (ids.isEmpty()) return null;
+		db.lowerRequestedFlag(txn, c, ids);
+		transaction.attach(new MessagesSentEvent(c, ids));
 		return Collections.unmodifiableList(messages);
 	}
 
-	public Offer generateOffer(ContactId c, int maxMessages, int maxLatency)
-			throws DbException {
-		Collection<MessageId> ids;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public Offer generateOffer(Transaction transaction, ContactId c,
+			int maxMessages, int maxLatency) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		Collection<MessageId> ids = db.getMessagesToOffer(txn, c, maxMessages);
 		if (ids.isEmpty()) return null;
+		for (MessageId m : ids) db.updateExpiryTime(txn, c, m, maxLatency);
 		return new Offer(ids);
 	}
 
-	public Request generateRequest(ContactId c, int maxMessages)
-			throws DbException {
-		Collection<MessageId> ids;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	public Request generateRequest(Transaction transaction, ContactId c,
+			int maxMessages) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		Collection<MessageId> ids = db.getMessagesToRequest(txn, c,
+				maxMessages);
 		if (ids.isEmpty()) return null;
+		db.removeOfferedMessages(txn, c, ids);
 		return new Request(ids);
 	}
 
-	public Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
-			int maxLatency) throws DbException {
-		Collection<MessageId> ids;
-		List<byte[]> messages = new ArrayList<byte[]>();
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public Collection<byte[]> generateRequestedBatch(Transaction transaction,
+			ContactId c, int maxLength, int maxLatency) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		Collection<MessageId> ids = db.getRequestedMessagesToSend(txn, c,
+				maxLength);
+		List<byte[]> messages = new ArrayList<byte[]>(ids.size());
+		for (MessageId m : ids) {
+			messages.add(db.getRawMessage(txn, m));
+			db.updateExpiryTime(txn, c, m, maxLatency);
 		}
-		if (messages.isEmpty()) return null;
-		if (!ids.isEmpty()) eventBus.broadcast(new MessagesSentEvent(c, ids));
+		if (ids.isEmpty()) return null;
+		db.lowerRequestedFlag(txn, c, ids);
+		transaction.attach(new MessagesSentEvent(c, ids));
 		return Collections.unmodifiableList(messages);
 	}
 
-	public Contact getContact(ContactId c) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
-	}
-
-	public Collection<Contact> getContacts() throws DbException {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<Contact> contacts = db.getContacts(txn);
-				db.commitTransaction(txn);
-				return contacts;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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<ContactId> getContacts(AuthorId a) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	public Collection<Contact> getContacts(Transaction transaction)
+			throws DbException {
+		T txn = unbox(transaction);
+		return db.getContacts(txn);
 	}
 
-	public DeviceId getDeviceId() throws DbException {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				DeviceId id = db.getDeviceId(txn);
-				db.commitTransaction(txn);
-				return id;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 Group getGroup(GroupId g) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	public DeviceId getDeviceId(Transaction transaction) throws DbException {
+		T txn = unbox(transaction);
+		return db.getDeviceId(txn);
 	}
 
-	public Metadata getGroupMetadata(GroupId g) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<LocalAuthor> authors = db.getLocalAuthors(txn);
-				db.commitTransaction(txn);
-				return authors;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+		T txn = unbox(transaction);
+		return db.getLocalAuthors(txn);
 	}
 
-	public byte[] getRawMessage(MessageId m) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getRawMessage(txn, m);
 	}
 
-	public Metadata getMessageMetadata(MessageId m) throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+		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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Settings s = db.getSettings(txn, namespace);
-				db.commitTransaction(txn);
-				return s;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+		T txn = unbox(transaction);
+		return db.getSettings(txn, namespace);
 	}
 
-	public Map<TransportId, Integer> getTransportLatencies()
-			throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 Collection<ContactId> getVisibility(GroupId g) throws DbException {
-		lock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsGroup(txn, g))
-					throw new NoSuchGroupException();
-				Collection<ContactId> visible = db.getVisibility(txn, g);
-				db.commitTransaction(txn);
-				return visible;
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	public Map<TransportId, Integer> getTransportLatencies(
+			Transaction transaction) throws DbException {
+		T txn = unbox(transaction);
+		return db.getTransportLatencies(txn);
 	}
 
-	public void incrementStreamCounter(ContactId c, TransportId t,
-			long rotationPeriod) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	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 boolean isVisibleToContact(ContactId c, GroupId g)
-			throws DbException {
-		lock.readLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.readLock().unlock();
-		}
+	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 mergeGroupMetadata(GroupId g, Metadata meta)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	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(MessageId m, Metadata meta)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	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 {
-		boolean changed = false;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
+	public void mergeSettings(Transaction transaction, Settings s,
+			String namespace) throws DbException {
+		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);
+			transaction.attach(new SettingsUpdatedEvent(namespace));
 		}
-		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 {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
 		Collection<MessageId> acked = new ArrayList<MessageId>();
-		lock.writeLock().lock();
-		try {
-			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);
-					}
-				}
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
+		for (MessageId m : a.getMessageIds()) {
+			if (db.containsVisibleMessage(txn, c, m)) {
+				db.raiseSeenFlag(txn, c, m);
+				acked.add(m);
 			}
-		} finally {
-			lock.writeLock().unlock();
 		}
-		eventBus.broadcast(new MessagesAckedEvent(c, acked));
+		transaction.attach(new MessagesAckedEvent(c, acked));
 	}
 
-	public void receiveMessage(ContactId c, Message m) throws DbException {
-		boolean duplicate, visible;
-		lock.writeLock().lock();
-		try {
-			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;
+	public void receiveMessage(Transaction transaction, ContactId c, Message m)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (db.containsVisibleGroup(txn, c, m.getGroupId())) {
+			if (!db.containsMessage(txn, m.getId())) {
+				addMessage(txn, m, UNKNOWN, false, c);
+				transaction.attach(new MessageAddedEvent(m, c));
 			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (visible) {
-			if (!duplicate) eventBus.broadcast(new MessageAddedEvent(m, c));
-			eventBus.broadcast(new MessageToAckEvent(c));
+			db.raiseAckFlag(txn, c, m.getId());
+			transaction.attach(new MessageToAckEvent(c));
 		}
 	}
 
-	public void receiveOffer(ContactId c, Offer o) throws DbException {
+	public void receiveOffer(Transaction transaction, ContactId c, Offer o)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		int count = db.countOfferedMessages(txn, c);
 		boolean ack = false, request = false;
-		lock.writeLock().lock();
-		try {
-			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++;
-					}
-				}
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
+		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++;
 			}
-		} finally {
-			lock.writeLock().unlock();
 		}
-		if (ack) eventBus.broadcast(new MessageToAckEvent(c));
-		if (request) eventBus.broadcast(new MessageToRequestEvent(c));
+		if (ack) transaction.attach(new MessageToAckEvent(c));
+		if (request) transaction.attach(new MessageToRequestEvent(c));
 	}
 
-	public void receiveRequest(ContactId c, Request r) throws DbException {
+	public void receiveRequest(Transaction transaction, ContactId c, Request r)
+			throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
 		boolean requested = false;
-		lock.writeLock().lock();
-		try {
-			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;
-					}
-				}
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (requested) eventBus.broadcast(new MessageRequestedEvent(c));
-	}
-
-	public void removeContact(ContactId c) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public void removeGroup(Group g) throws DbException {
-		Collection<ContactId> affected;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		eventBus.broadcast(new GroupRemovedEvent(g));
-		eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
-	}
-
-	public void removeLocalAuthor(AuthorId a) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
-	public void removeTransport(TransportId t) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
+		for (MessageId m : r.getMessageIds()) {
+			if (db.containsVisibleMessage(txn, c, m)) {
+				db.raiseRequestedFlag(txn, c, m);
+				db.resetExpiryTime(txn, c, m);
+				requested = true;
 			}
-		} finally {
-			lock.writeLock().unlock();
 		}
-		eventBus.broadcast(new TransportRemovedEvent(t));
+		if (requested) transaction.attach(new MessageRequestedEvent(c));
 	}
 
-	public void setContactStatus(ContactId c, StorageStatus s)
+	public void removeContact(Transaction transaction, ContactId c)
 			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		db.removeContact(txn, c);
+		transaction.attach(new ContactRemovedEvent(c));
 	}
 
-	public void setLocalAuthorStatus(AuthorId a, StorageStatus s)
+	public void removeGroup(Transaction transaction, Group g)
 			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+		T txn = unbox(transaction);
+		GroupId id = g.getId();
+		if (!db.containsGroup(txn, id))
+			throw new NoSuchGroupException();
+		Collection<ContactId> affected = db.getVisibility(txn, id);
+		db.removeGroup(txn, id);
+		transaction.attach(new GroupRemovedEvent(g));
+		transaction.attach(new GroupVisibilityUpdatedEvent(affected));
 	}
 
-	public void setMessageShared(Message m, boolean shared)
+	public void removeLocalAuthor(Transaction transaction, AuthorId a)
 			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (shared) eventBus.broadcast(new MessageSharedEvent(m));
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, a))
+			throw new NoSuchLocalAuthorException();
+		db.removeLocalAuthor(txn, a);
+		transaction.attach(new LocalAuthorRemovedEvent(a));
 	}
 
-	public void setMessageValid(Message m, ClientId c, boolean valid)
+	public void removeTransport(Transaction transaction, TransportId t)
 			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		eventBus.broadcast(new MessageValidatedEvent(m, c, false, valid));
+		T txn = unbox(transaction);
+		if (!db.containsTransport(txn, t))
+			throw new NoSuchTransportException();
+		db.removeTransport(txn, t);
+		transaction.attach(new TransportRemovedEvent(t));
 	}
 
-	public void setReorderingWindow(ContactId c, TransportId t,
-			long rotationPeriod, long base, byte[] bitmap) throws DbException {
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+	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) transaction.attach(new MessageSharedEvent(m));
 	}
 
-	public void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException {
-		Collection<ContactId> affected = new ArrayList<ContactId>();
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsGroup(txn, g))
-					throw new NoSuchGroupException();
-				// Use HashSets for O(1) lookups, O(n) overall running time
-				Collection<ContactId> now = new HashSet<ContactId>(visible);
-				Collection<ContactId> before = db.getVisibility(txn, g);
-				before = new HashSet<ContactId>(before);
-				// Set the group's visibility for each current contact
-				for (ContactId c : db.getContactIds(txn)) {
-					boolean wasBefore = before.contains(c);
-					boolean isNow = now.contains(c);
-					if (!wasBefore && isNow) {
-						db.addVisibility(txn, c, g);
-						affected.add(c);
-					} else if (wasBefore && !isNow) {
-						db.removeVisibility(txn, c, g);
-						affected.add(c);
-					}
-				}
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-		if (!affected.isEmpty())
-			eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
+	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);
+		transaction.attach(new MessageValidatedEvent(m, c, false, valid));
 	}
 
-	public void setVisibleToContact(ContactId c, GroupId g, boolean visible)
+	public void setReorderingWindow(Transaction transaction, ContactId c,
+			TransportId t, long rotationPeriod, long base, byte[] bitmap)
 			throws DbException {
-		boolean wasVisible = false;
-		lock.writeLock().lock();
-		try {
-			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;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
+		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(Transaction transaction, ContactId c,
+			GroupId g, boolean visible) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		boolean 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)));
+			List<ContactId> affected = Collections.singletonList(c);
+			transaction.attach(new GroupVisibilityUpdatedEvent(affected));
 		}
 	}
 
-	public void updateTransportKeys(Map<ContactId, TransportKeys> keys)
-			throws DbException {
-		lock.writeLock().lock();
-		try {
-			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);
-					}
-				}
-				db.updateTransportKeys(txn, filtered);
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
+	public void updateTransportKeys(Transaction transaction,
+			Map<ContactId, TransportKeys> keys) throws DbException {
+		T txn = unbox(transaction);
+		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);
 			}
-		} finally {
-			lock.writeLock().unlock();
 		}
+		db.updateTransportKeys(txn, filtered);
 	}
 }
diff --git a/briar-core/src/org/briarproject/db/DatabaseModule.java b/briar-core/src/org/briarproject/db/DatabaseModule.java
index 9ac531c0775312fd735d2127144f616646e1394d..6a050ed44b9a449fc8d2e5f5ec461ef749f61043 100644
--- a/briar-core/src/org/briarproject/db/DatabaseModule.java
+++ b/briar-core/src/org/briarproject/db/DatabaseModule.java
@@ -51,7 +51,8 @@ public class DatabaseModule extends AbstractModule {
 	@Provides @Singleton
 	DatabaseComponent getDatabaseComponent(Database<Connection> db,
 			EventBus eventBus, ShutdownManager shutdown) {
-		return new DatabaseComponentImpl<Connection>(db, eventBus, shutdown);
+		return new DatabaseComponentImpl<Connection>(db, Connection.class,
+				eventBus, shutdown);
 	}
 
 	@Provides @Singleton @DatabaseExecutor
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index ac583594bd27844440519e3c9256cab770b06409..4af26f32006d3e0c44b62e05cc639757baad8a42 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -9,7 +9,6 @@ import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DbClosedException;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
-import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
@@ -41,7 +40,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -49,7 +47,6 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.db.Metadata.REMOVE;
-import static org.briarproject.api.db.StorageStatus.ADDING;
 import static org.briarproject.api.sync.ValidationManager.Validity.INVALID;
 import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
 import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
@@ -66,8 +63,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 20;
-	private static final int MIN_SCHEMA_VERSION = 20;
+	private static final int SCHEMA_VERSION = 21;
+	private static final int MIN_SCHEMA_VERSION = 21;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -83,7 +80,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " publicKey BINARY NOT NULL,"
 					+ " privateKey BINARY NOT NULL,"
 					+ " created BIGINT NOT NULL,"
-					+ " status INT NOT NULL,"
 					+ " PRIMARY KEY (authorId))";
 
 	private static final String CREATE_CONTACTS =
@@ -93,7 +89,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " name VARCHAR NOT NULL,"
 					+ " publicKey BINARY NOT NULL,"
 					+ " localAuthorId HASH NOT NULL,"
-					+ " status INT NOT NULL,"
 					+ " PRIMARY KEY (contactId),"
 					+ " FOREIGN KEY (localAuthorId)"
 					+ " REFERENCES localAuthors (authorId)"
@@ -228,8 +223,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private final LinkedList<Connection> connections =
 			new LinkedList<Connection>(); // Locking: connectionsLock
 
-	private final AtomicInteger transactionCount = new AtomicInteger(0);
-
 	private int openConnections = 0; // Locking: connectionsLock
 	private boolean closed = false; // Locking: connectionsLock
 
@@ -369,7 +362,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		} catch (SQLException e) {
 			throw new DbException(e);
 		}
-		transactionCount.incrementAndGet();
 		return txn;
 	}
 
@@ -418,14 +410,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public int getTransactionCount() {
-		return transactionCount.get();
-	}
-
-	public void resetTransactionCount() {
-		transactionCount.set(0);
-	}
-
 	protected void closeAllConnections() throws SQLException {
 		boolean interrupted = false;
 		connectionsLock.lock();
@@ -464,14 +448,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Create a contact row
 			String sql = "INSERT INTO contacts"
-					+ " (authorId, name, publicKey, localAuthorId, status)"
-					+ " VALUES (?, ?, ?, ?, ?)";
+					+ " (authorId, name, publicKey, localAuthorId)"
+					+ " VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, remote.getId().getBytes());
 			ps.setString(2, remote.getName());
 			ps.setBytes(3, remote.getPublicKey());
 			ps.setBytes(4, local.getBytes());
-			ps.setInt(5, ADDING.getValue());
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -540,16 +523,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "INSERT INTO localAuthors (authorId, name, publicKey,"
-					+ " privateKey, created, status)"
-					+ " VALUES (?, ?, ?, ?, ?, ?)";
+			String sql = "INSERT INTO localAuthors"
+					+ " (authorId, name, publicKey, privateKey, created)"
+					+ " VALUES (?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, a.getId().getBytes());
 			ps.setString(2, a.getName());
 			ps.setBytes(3, a.getPublicKey());
 			ps.setBytes(4, a.getPrivateKey());
 			ps.setLong(5, a.getTimeCreated());
-			ps.setInt(6, a.getStatus().getValue());
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -969,8 +951,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT authorId, name, publicKey, localAuthorId,"
-					+ " status"
+			String sql = "SELECT authorId, name, publicKey, localAuthorId"
 					+ " FROM contacts"
 					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
@@ -981,11 +962,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 			String name = rs.getString(2);
 			byte[] publicKey = rs.getBytes(3);
 			AuthorId localAuthorId = new AuthorId(rs.getBytes(4));
-			StorageStatus status = StorageStatus.fromValue(rs.getInt(5));
 			rs.close();
 			ps.close();
 			Author author = new Author(authorId, name, publicKey);
-			return new Contact(c, author, localAuthorId, status);
+			return new Contact(c, author, localAuthorId);
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1019,7 +999,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT contactId, authorId, name, publicKey,"
-					+ " localAuthorId, status"
+					+ " localAuthorId"
 					+ " FROM contacts";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
@@ -1031,9 +1011,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				byte[] publicKey = rs.getBytes(4);
 				Author author = new Author(authorId, name, publicKey);
 				AuthorId localAuthorId = new AuthorId(rs.getBytes(5));
-				StorageStatus status = StorageStatus.fromValue(rs.getInt(6));
-				contacts.add(new Contact(contactId, author, localAuthorId,
-						status));
+				contacts.add(new Contact(contactId, author, localAuthorId));
 			}
 			rs.close();
 			ps.close();
@@ -1120,7 +1098,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT name, publicKey, privateKey, created, status"
+			String sql = "SELECT name, publicKey, privateKey, created"
 					+ " FROM localAuthors"
 					+ " WHERE authorId = ?";
 			ps = txn.prepareStatement(sql);
@@ -1131,9 +1109,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			byte[] publicKey = rs.getBytes(2);
 			byte[] privateKey = rs.getBytes(3);
 			long created = rs.getLong(4);
-			StorageStatus status = StorageStatus.fromValue(rs.getInt(5));
 			LocalAuthor localAuthor = new LocalAuthor(a, name, publicKey,
-					privateKey, created, status);
+					privateKey, created);
 			if (rs.next()) throw new DbStateException();
 			rs.close();
 			ps.close();
@@ -1150,8 +1127,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT authorId, name, publicKey, privateKey,"
-					+ " created, status"
+			String sql = "SELECT authorId, name, publicKey, privateKey, created"
 					+ " FROM localAuthors";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
@@ -1162,9 +1138,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 				byte[] publicKey = rs.getBytes(3);
 				byte[] privateKey = rs.getBytes(4);
 				long created = rs.getLong(5);
-				StorageStatus status = StorageStatus.fromValue(rs.getInt(6));
 				authors.add(new LocalAuthor(authorId, name, publicKey,
-						privateKey, created, status));
+						privateKey, created));
 			}
 			rs.close();
 			ps.close();
@@ -2053,41 +2028,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setContactStatus(Connection txn, ContactId c, StorageStatus s)
-			throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "UPDATE contacts SET status = ? WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, s.getValue());
-			ps.setInt(2, c.getInt());
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public void setLocalAuthorStatus(Connection txn, AuthorId a,
-			StorageStatus s) throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "UPDATE localAuthors SET status = ?"
-					+ " WHERE authorId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, s.getValue());
-			ps.setBytes(2, a.getBytes());
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public void setMessageShared(Connection txn, MessageId m, boolean shared)
 			throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 0e2c581c84e293d97fbe61eb8f6ca947ad6cea18..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,6 +12,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.ForumPost;
@@ -36,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;
@@ -58,20 +57,14 @@ class ForumManagerImpl implements ForumManager {
 			Logger.getLogger(ForumManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final ContactManager contactManager;
 	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,
-			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
-			MetadataParser metadataParser) {
+	ForumManagerImpl(DatabaseComponent db, BdfReaderFactory bdfReaderFactory,
+			MetadataEncoder metadataEncoder, MetadataParser metadataParser) {
 		this.db = db;
-		this.contactManager = contactManager;
 		this.bdfReaderFactory = bdfReaderFactory;
 		this.metadataEncoder = metadataEncoder;
 		this.metadataParser = metadataParser;
@@ -84,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());
@@ -102,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);
@@ -161,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 : db.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 5218e5388ac8dff27fd39f37621b7dffe0a11b10..c3ab558dec041169d246ba3a485322438317b856 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,7 @@ 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 +59,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 +72,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 +95,39 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 	}
 
 	@Override
-	public void addingContact(ContactId 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(db.getContact(c));
+			Group g = getContactGroup(c);
 			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibility(g.getId(), Collections.singletonList(c));
+			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.getInt());
-			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
+			d.put("contactId", c.getId().getInt());
+			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(ContactId c) {
-		lock.writeLock().lock();
-		try {
-			db.removeGroup(getContactGroup(db.getContact(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 +150,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 +313,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 +366,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);
+			Metadata meta = metadataEncoder.encode(d);
+			db.addLocalMessage(txn, m, CLIENT_ID, meta, true);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
 	}
 
 	private byte[] encodeForumList(List<Forum> forums, long version) {
@@ -418,23 +404,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 +427,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 +474,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/sync/AuthorFactoryImpl.java b/briar-core/src/org/briarproject/identity/AuthorFactoryImpl.java
similarity index 89%
rename from briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java
rename to briar-core/src/org/briarproject/identity/AuthorFactoryImpl.java
index bfb768802179b112b345d8d675b9d8acc211c79d..b745f5e003b5f388d8c6de4a12fb04e705579bac 100644
--- a/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java
+++ b/briar-core/src/org/briarproject/identity/AuthorFactoryImpl.java
@@ -1,4 +1,4 @@
-package org.briarproject.sync;
+package org.briarproject.identity;
 
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.BdfWriter;
@@ -14,9 +14,6 @@ import java.io.IOException;
 
 import javax.inject.Inject;
 
-import static org.briarproject.api.db.StorageStatus.ADDING;
-
-// TODO: Move this class to the identity package
 class AuthorFactoryImpl implements AuthorFactory {
 
 	private final CryptoComponent crypto;
@@ -38,7 +35,7 @@ class AuthorFactoryImpl implements AuthorFactory {
 	public LocalAuthor createLocalAuthor(String name, byte[] publicKey,
 			byte[] privateKey) {
 		return new LocalAuthor(getId(name, publicKey), name, publicKey,
-				privateKey, clock.currentTimeMillis(), ADDING);
+				privateKey, clock.currentTimeMillis());
 	}
 
 	private AuthorId getId(String name, byte[] publicKey) {
diff --git a/briar-core/src/org/briarproject/sync/AuthorReader.java b/briar-core/src/org/briarproject/identity/AuthorReader.java
similarity index 92%
rename from briar-core/src/org/briarproject/sync/AuthorReader.java
rename to briar-core/src/org/briarproject/identity/AuthorReader.java
index cb697681eebfdf0d318679625fdb6a98d87a6c7b..16be01ae63951150979a90bc28354a1f2d6c33cc 100644
--- a/briar-core/src/org/briarproject/sync/AuthorReader.java
+++ b/briar-core/src/org/briarproject/identity/AuthorReader.java
@@ -1,4 +1,4 @@
-package org.briarproject.sync;
+package org.briarproject.identity;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.data.BdfReader;
@@ -11,7 +11,6 @@ import java.io.IOException;
 import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 
-// TODO: Move this class to the identity package
 class AuthorReader implements ObjectReader<Author> {
 
 	private final AuthorFactory authorFactory;
diff --git a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
index a51ce023880530c5ffc915823a78d2209fe8271c..0ae2e5aa584958e721a91befb21d0ef648337a0a 100644
--- a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
+++ b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java
@@ -4,74 +4,28 @@ 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.event.EventBus;
-import org.briarproject.api.event.LocalAuthorAddedEvent;
-import org.briarproject.api.event.LocalAuthorRemovedEvent;
+import org.briarproject.api.db.Transaction;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.lifecycle.Service;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.logging.Logger;
 
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.db.StorageStatus.ACTIVE;
-import static org.briarproject.api.db.StorageStatus.ADDING;
-import static org.briarproject.api.db.StorageStatus.REMOVING;
-
-class IdentityManagerImpl implements IdentityManager, Service {
-
-	private static final Logger LOG =
-			Logger.getLogger(IdentityManagerImpl.class.getName());
+class IdentityManagerImpl implements IdentityManager {
 
 	private final DatabaseComponent db;
-	private final EventBus eventBus;
 	private final List<AddIdentityHook> addHooks;
 	private final List<RemoveIdentityHook> removeHooks;
 
 	@Inject
-	IdentityManagerImpl(DatabaseComponent db, EventBus eventBus) {
+	IdentityManagerImpl(DatabaseComponent db) {
 		this.db = db;
-		this.eventBus = eventBus;
 		addHooks = new CopyOnWriteArrayList<AddIdentityHook>();
 		removeHooks = new CopyOnWriteArrayList<RemoveIdentityHook>();
 	}
 
-	@Override
-	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.getId());
-					db.setLocalAuthorStatus(a.getId(), ACTIVE);
-					eventBus.broadcast(new LocalAuthorAddedEvent(a.getId()));
-				} else if (a.getStatus().equals(REMOVING)) {
-					for (RemoveIdentityHook hook : removeHooks)
-						hook.removingIdentity(a.getId());
-					db.removeLocalAuthor(a.getId());
-					eventBus.broadcast(new LocalAuthorRemovedEvent(a.getId()));
-				}
-			}
-			return true;
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			return false;
-		}
-	}
-
-	@Override
-	public boolean stop() {
-		return false;
-	}
-
 	@Override
 	public void registerAddIdentityHook(AddIdentityHook hook) {
 		addHooks.add(hook);
@@ -83,35 +37,55 @@ class IdentityManagerImpl implements IdentityManager, Service {
 	}
 
 	@Override
-	public void addLocalAuthor(LocalAuthor a) throws DbException {
-		db.addLocalAuthor(a);
-		for (AddIdentityHook hook : addHooks) hook.addingIdentity(a.getId());
-		db.setLocalAuthorStatus(a.getId(), ACTIVE);
-		eventBus.broadcast(new LocalAuthorAddedEvent(a.getId()));
+	public void addLocalAuthor(LocalAuthor localAuthor) throws DbException {
+		Transaction txn = db.startTransaction();
+		try {
+			db.addLocalAuthor(txn, localAuthor);
+			for (AddIdentityHook hook : addHooks)
+				hook.addingIdentity(txn, localAuthor);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 	}
 
 	@Override
 	public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
-		LocalAuthor author = db.getLocalAuthor(a);
-		if (author.getStatus().equals(ACTIVE)) return author;
-		throw new NoSuchLocalAuthorException();
+		LocalAuthor author;
+		Transaction txn = db.startTransaction();
+		try {
+			author = db.getLocalAuthor(txn, a);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return author;
 	}
 
 	@Override
 	public Collection<LocalAuthor> getLocalAuthors() throws DbException {
-		Collection<LocalAuthor> authors = db.getLocalAuthors();
-		// Filter out any pseudonyms that are being added or removed
-		List<LocalAuthor> active = new ArrayList<LocalAuthor>(authors.size());
-		for (LocalAuthor a : authors)
-			if (a.getStatus().equals(ACTIVE)) active.add(a);
-		return Collections.unmodifiableList(active);
+		Collection<LocalAuthor> authors;
+		Transaction txn = db.startTransaction();
+		try {
+			authors = db.getLocalAuthors(txn);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return authors;
 	}
 
 	@Override
 	public void removeLocalAuthor(AuthorId a) throws DbException {
-		db.setLocalAuthorStatus(a, REMOVING);
-		for (RemoveIdentityHook hook : removeHooks) hook.removingIdentity(a);
-		db.removeLocalAuthor(a);
-		eventBus.broadcast(new LocalAuthorRemovedEvent(a));
+		Transaction txn = db.startTransaction();
+		try {
+			LocalAuthor localAuthor = db.getLocalAuthor(txn, a);
+			for (RemoveIdentityHook hook : removeHooks)
+				hook.removingIdentity(txn, localAuthor);
+			db.removeLocalAuthor(txn, a);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/identity/IdentityModule.java b/briar-core/src/org/briarproject/identity/IdentityModule.java
index 98134d1992aa5edabaa208719053648dca3fc83b..c987a8a55addf7c980f145c60842a82858e45b03 100644
--- a/briar-core/src/org/briarproject/identity/IdentityModule.java
+++ b/briar-core/src/org/briarproject/identity/IdentityModule.java
@@ -1,13 +1,23 @@
 package org.briarproject.identity;
 
 import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
 
+import org.briarproject.api.data.ObjectReader;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
 
 public class IdentityModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
+		bind(AuthorFactory.class).to(AuthorFactoryImpl.class);
 		bind(IdentityManager.class).to(IdentityManagerImpl.class);
 	}
+
+	@Provides
+	ObjectReader<Author> getAuthorReader(AuthorFactory authorFactory) {
+		return new AuthorReader(authorFactory);
+	}
 }
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 65f0432893f5d5d54c9806719b8d2be007f92cb6..ef566f2b16e6c48d831d9166f8cbe282e21d7f88 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;
@@ -32,7 +31,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 import java.util.logging.Logger;
 
@@ -51,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;
@@ -71,19 +67,17 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	}
 
 	@Override
-	public void addingContact(ContactId c) {
+	public void addingContact(Transaction txn, Contact c) throws DbException {
 		try {
 			// Create a group to share with the contact
-			Group g = getContactGroup(db.getContact(c));
+			Group g = getContactGroup(c);
 			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibility(g.getId(), Collections.singletonList(c));
+			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.getInt());
-			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
-		} catch (DbException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			d.put("contactId", c.getId().getInt());
+			db.mergeGroupMetadata(txn, g.getId(), metadataEncoder.encode(d));
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -94,12 +88,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	}
 
 	@Override
-	public void removingContact(ContactId c) {
-		try {
-			db.removeGroup(getContactGroup(db.getContact(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
@@ -109,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);
 		}
@@ -126,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);
+		Map<MessageId, Metadata> metadata;
+		Collection<MessageStatus> statuses;
+		Transaction txn = db.startTransaction();
+		try {
+			GroupId g = getContactGroup(db.getContact(txn, c)).getId();
+			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) {
@@ -169,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);
@@ -192,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 bbc6cd57d16f68433e7debf2e87977ced7094c3e..b3db2611c4189027de88153134e6766256451d84 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,156 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	}
 
 	@Override
-	public void addingContact(ContactId c) {
-		lock.writeLock().lock();
-		try {
-			// Create a group to share with the contact
-			Group g = getContactGroup(db.getContact(c));
-			// Store the group and share it with the contact
-			db.addGroup(g);
-			db.setVisibility(g.getId(), Collections.singletonList(c));
-			// 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(txn);
+		for (Entry<TransportId, TransportProperties> e : local.entrySet()) {
+			storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 1,
+					true, true);
 		}
 	}
 
 	@Override
-	public void removingContact(ContactId c) {
-		lock.writeLock().lock();
-		try {
-			db.removeGroup(getContactGroup(db.getContact(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(db.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();
+		Map<TransportId, TransportProperties> local;
+		Transaction txn = db.startTransaction();
 		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));
-			}
-			return Collections.unmodifiableMap(local);
-		} catch (NoSuchGroupException e) {
-			// Local group doesn't exist - there are no local properties
-			return Collections.emptyMap();
-		} catch (IOException e) {
-			throw new DbException(e);
+			local = getLocalProperties(txn);
+			txn.setComplete();
 		} finally {
-			lock.readLock().unlock();
+			db.endTransaction(txn);
 		}
+		return Collections.unmodifiableMap(local);
 	}
 
 	@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 +240,44 @@ 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 Map<TransportId, TransportProperties> getLocalProperties(
+			Transaction txn) throws DbException {
+		try {
+			Map<TransportId, TransportProperties> local =
+					new HashMap<TransportId, TransportProperties>();
+			// 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));
+			}
+			return local;
+		} catch (NoSuchGroupException e) {
+			// Local group doesn't exist - there are no local properties
+			return Collections.emptyMap();
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	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 +298,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 +316,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 +333,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..eb929aa0991f76006c49e1be5ff19d5c7c66f532 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,25 @@ class SettingsManagerImpl implements SettingsManager {
 
 	@Override
 	public Settings getSettings(String namespace) throws DbException {
-		return db.getSettings(namespace);
+		Settings s;
+		Transaction txn = db.startTransaction();
+		try {
+			s = db.getSettings(txn, namespace);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return s;
 	}
 
 	@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/SyncModule.java b/briar-core/src/org/briarproject/sync/SyncModule.java
index 7a61c68a4e174f8848a270e8b7db9a4d7bbf18da..83c8f86ec503824edf796153b339aff133e0aed0 100644
--- a/briar-core/src/org/briarproject/sync/SyncModule.java
+++ b/briar-core/src/org/briarproject/sync/SyncModule.java
@@ -3,10 +3,7 @@ package org.briarproject.sync;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
-import org.briarproject.api.data.ObjectReader;
 import org.briarproject.api.event.EventBus;
-import org.briarproject.api.identity.Author;
-import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.MessageFactory;
@@ -22,7 +19,6 @@ public class SyncModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
-		bind(AuthorFactory.class).to(AuthorFactoryImpl.class);
 		bind(GroupFactory.class).to(GroupFactoryImpl.class);
 		bind(MessageFactory.class).to(MessageFactoryImpl.class);
 		bind(PacketReaderFactory.class).to(PacketReaderFactoryImpl.class);
@@ -32,11 +28,6 @@ public class SyncModule extends AbstractModule {
 				SyncSessionFactoryImpl.class).in(Singleton.class);
 	}
 
-	@Provides
-	ObjectReader<Author> getAuthorReader(AuthorFactory authorFactory) {
-		return new AuthorReader(authorFactory);
-	}
-
 	@Provides @Singleton
 	ValidationManager getValidationManager(LifecycleManager lifecycleManager,
 			EventBus eventBus, ValidationManagerImpl validationManager) {
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 5fca1458452be5ed09fc776e6de3d3f3b1c1f773..85f383bae4372b7cfdad2e91515b4e1df94e6a1e 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 {
+							db.mergeMessageMetadata(txn, m.getId(), meta);
+							db.setMessageValid(txn, m, c, true);
+							db.setMessageShared(txn, m, true);
+							for (ValidationHook hook : hooks)
+								hook.validatingMessage(txn, m, c, meta);
+						}
+						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/contact/ContactManagerImplTest.java b/briar-tests/src/org/briarproject/contact/ContactManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6cde435e38aa438ca360302eea9989c08b8c10fc
--- /dev/null
+++ b/briar-tests/src/org/briarproject/contact/ContactManagerImplTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.contact;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class ContactManagerImplTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 70a9113f23c5de966476e7efbb867f0d94236bc7..3488bb2d1a106e49b9f4cccdcb5ba3a6d3e40f1b 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -13,11 +13,15 @@ import org.briarproject.api.db.NoSuchGroupException;
 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.ContactAddedEvent;
+import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.GroupAddedEvent;
 import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
+import org.briarproject.api.event.LocalAuthorAddedEvent;
+import org.briarproject.api.event.LocalAuthorRemovedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
@@ -50,11 +54,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;
@@ -90,8 +96,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		localAuthorId = new AuthorId(TestUtils.getRandomId());
 		long timestamp = System.currentTimeMillis();
 		localAuthor = new LocalAuthor(localAuthorId, "Bob",
-				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp,
-				StorageStatus.ACTIVE);
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
 		messageId = new MessageId(TestUtils.getRandomId());
 		messageId1 = new MessageId(TestUtils.getRandomId());
 		size = 1234;
@@ -102,13 +107,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		transportId = new TransportId("id");
 		maxLatency = Integer.MAX_VALUE;
 		contactId = new ContactId(234);
-		contact = new Contact(contactId, author, localAuthorId,
-				StorageStatus.ACTIVE);
+		contact = new Contact(contactId, author, localAuthorId);
 	}
 
-	private <T> DatabaseComponent createDatabaseComponent(Database<T> database,
+	private DatabaseComponent createDatabaseComponent(Database<Object> database,
 			EventBus eventBus, ShutdownManager shutdown) {
-		return new DatabaseComponentImpl<T>(database, eventBus, shutdown);
+		return new DatabaseComponentImpl<Object>(database, Object.class,
+				eventBus, shutdown);
 	}
 
 	@Test
@@ -120,18 +125,19 @@ 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));
 			oneOf(database).addLocalAuthor(txn, localAuthor);
+			oneOf(eventBus).broadcast(with(any(LocalAuthorAddedEvent.class)));
 			// addContact()
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(true));
@@ -139,6 +145,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).addContact(txn, author, localAuthorId);
 			will(returnValue(contactId));
+			oneOf(eventBus).broadcast(with(any(ContactAddedEvent.class)));
 			// getContacts()
 			oneOf(database).getContacts(txn);
 			will(returnValue(Collections.singletonList(contact)));
@@ -166,10 +173,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			oneOf(database).removeContact(txn, contactId);
+			oneOf(eventBus).broadcast(with(any(ContactRemovedEvent.class)));
 			// removeLocalAuthor()
 			oneOf(database).containsLocalAuthor(txn, localAuthorId);
 			will(returnValue(true));
 			oneOf(database).removeLocalAuthor(txn, localAuthorId);
+			oneOf(eventBus).broadcast(with(any(LocalAuthorRemovedEvent.class)));
+			// endTransaction()
+			oneOf(database).commitTransaction(txn);
 			// close()
 			oneOf(shutdown).removeShutdownHook(shutdownHandle);
 			oneOf(database).close();
@@ -178,15 +189,24 @@ 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();
+		try {
+			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();
+		} finally {
+			db.endTransaction(transaction);
+		}
 		db.close();
 
 		context.assertIsSatisfied();
@@ -210,11 +230,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();
@@ -252,7 +275,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();
 	}
@@ -276,126 +305,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();
@@ -420,25 +501,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();
@@ -454,11 +544,11 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the group is in the DB (which it's not)
-			exactly(9).of(database).startTransaction();
+			exactly(7).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(9).of(database).containsGroup(txn, groupId);
+			exactly(7).of(database).containsGroup(txn, groupId);
 			will(returnValue(false));
-			exactly(9).of(database).abortTransaction(txn);
+			exactly(7).of(database).abortTransaction(txn);
 			// This is needed for getMessageStatus(), isVisibleToContact(), and
 			// setVisibleToContact() to proceed
 			exactly(3).of(database).containsContact(txn, contactId);
@@ -467,67 +557,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.getVisibility(groupId);
+			db.isVisibleToContact(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.isVisibleToContact(contactId, groupId);
+			db.mergeGroupMetadata(transaction, groupId, metadata);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.mergeGroupMetadata(groupId, metadata);
+			db.removeGroup(transaction, group);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
+		transaction = db.startTransaction();
 		try {
-			db.removeGroup(group);
-			fail();
-		} catch (NoSuchGroupException expected) {
-			// Expected
-		}
-
-		try {
-			db.setVisibility(groupId, Collections.<ContactId>emptyList());
-			fail();
-		} catch (NoSuchGroupException expected) {
-			// Expected
-		}
-
-		try {
-			db.setVisibleToContact(contactId, groupId, true);
+			db.setVisibleToContact(transaction, contactId, groupId, true);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
+		} finally {
+			db.endTransaction(transaction);
 		}
 
 		context.assertIsSatisfied();
@@ -555,60 +652,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();
@@ -623,22 +744,23 @@ 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);
+			oneOf(eventBus).broadcast(with(any(LocalAuthorAddedEvent.class)));
 			// 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));
+			oneOf(eventBus).broadcast(with(any(ContactAddedEvent.class)));
+			// endTransaction()
 			oneOf(database).commitTransaction(txn);
 			// Check whether the transport is in the DB (which it's not)
 			exactly(4).of(database).startTransaction();
@@ -652,35 +774,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();
@@ -708,8 +850,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();
 	}
@@ -746,8 +894,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();
 	}
@@ -777,8 +931,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();
 	}
@@ -805,8 +965,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();
 	}
@@ -844,8 +1010,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();
 	}
@@ -871,7 +1043,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();
 	}
@@ -909,7 +1088,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();
 	}
@@ -939,7 +1124,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();
 	}
@@ -956,8 +1147,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).containsMessage(txn, messageId);
-			will(returnValue(false));
 			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
@@ -965,7 +1154,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();
 	}
@@ -1011,9 +1206,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();
 	}
 
@@ -1039,16 +1241,20 @@ 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();
 	}
 
 	@Test
 	public void testChangingVisibilityCallsListeners() throws Exception {
-		final ContactId contactId1 = new ContactId(123);
-		final Collection<ContactId> both = Arrays.asList(contactId, contactId1);
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1057,13 +1263,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(both));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
-			oneOf(database).removeVisibility(txn, contactId1, groupId);
+			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
+			will(returnValue(false)); // Not yet visible
+			oneOf(database).addVisibility(txn, contactId, groupId);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(
 					GroupVisibilityUpdatedEvent.class)));
@@ -1071,7 +1277,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.setVisibility(groupId, Collections.singletonList(contactId));
+		Transaction transaction = db.startTransaction();
+		try {
+			db.setVisibleToContact(transaction, contactId, groupId, true);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -1079,8 +1291,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 	@Test
 	public void testNotChangingVisibilityDoesNotCallListeners()
 			throws Exception {
-		final ContactId contactId1 = new ContactId(123);
-		final Collection<ContactId> both = Arrays.asList(contactId, contactId1);
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1089,56 +1299,67 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(both));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
+			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
+			will(returnValue(true)); // Already visible
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.setVisibility(groupId, both);
+		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();
 	}
@@ -1179,29 +1400,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/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index 1e880356b2adddadf63a084acb8b6a7223f5c4a8..34a91d63c0e54037593038b7d2795c2df5b7fac3 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -8,7 +8,6 @@ import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
-import org.briarproject.api.db.StorageStatus;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
@@ -87,8 +86,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		localAuthorId = new AuthorId(TestUtils.getRandomId());
 		timestamp = System.currentTimeMillis();
 		localAuthor = new LocalAuthor(localAuthorId, "Bob",
-				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp,
-				StorageStatus.ACTIVE);
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
 		messageId = new MessageId(TestUtils.getRandomId());
 		size = 1234;
 		raw = new byte[size];
@@ -1060,8 +1058,7 @@ public class H2DatabaseTest extends BriarTestCase {
 			throws Exception {
 		AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
 		LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
-				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp,
-				StorageStatus.ACTIVE);
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
 
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
diff --git a/briar-tests/src/org/briarproject/identity/IdentityManagerImplTest.java b/briar-tests/src/org/briarproject/identity/IdentityManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c04567a0a41253fe1b91eb0bf76d41a50d8aed10
--- /dev/null
+++ b/briar-tests/src/org/briarproject/identity/IdentityManagerImplTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.identity;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class IdentityManagerImplTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
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..2c3cc4348ffaaf77234df32c5ef3e0f5b711ba93 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
@@ -12,7 +12,7 @@ import org.briarproject.api.contact.ContactId;
 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,11 +120,16 @@ 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,
-				StorageStatus.ADDING);
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
 		identityManager.addLocalAuthor(aliceAuthor);
 		// Add Bob as a contact
 		Author bobAuthor = new Author(bobId, "Bob",
@@ -185,11 +190,16 @@ 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,
-				StorageStatus.ADDING);
+				new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp);
 		identityManager.addLocalAuthor(bobAuthor);
 		// Add Alice as a contact
 		Author aliceAuthor = new Author(aliceId, "Alice",
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