diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
index 5f196f147060a08fee698f2e4747c6f0609dc074..a6e3474f30453b24d06b0b4aaeeb72a96d0a89bd 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/SyncConstants.java
@@ -2,6 +2,8 @@ package org.briarproject.bramble.api.sync;
 
 import org.briarproject.bramble.api.UniqueId;
 
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
+
 public interface SyncConstants {
 
 	/**
@@ -9,16 +11,6 @@ public interface SyncConstants {
 	 */
 	byte PROTOCOL_VERSION = 0;
 
-	/**
-	 * The length of the record header in bytes.
-	 */
-	int RECORD_HEADER_LENGTH = 4;
-
-	/**
-	 * The maximum length of the record payload in bytes.
-	 */
-	int MAX_RECORD_PAYLOAD_LENGTH = 48 * 1024; // 48 KiB
-
 	/**
 	 * The maximum length of a group descriptor in bytes.
 	 */
@@ -42,5 +34,5 @@ public interface SyncConstants {
 	/**
 	 * The maximum number of message IDs in an ack, offer or request record.
 	 */
-	int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_LENGTH / UniqueId.LENGTH;
+	int MAX_MESSAGE_IDS = MAX_RECORD_PAYLOAD_BYTES / UniqueId.LENGTH;
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
index 8831ae4719c7ccc3da6a1758b852178662fb52cb..c2b54f5a9722fe2e8378f0421df6f45e16a7a805 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java
@@ -39,8 +39,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
 
 /**
  * An outgoing {@link SyncSession} suitable for duplex transports. The session
@@ -273,7 +273,7 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 				Transaction txn = db.startTransaction(false);
 				try {
 					b = db.generateRequestedBatch(txn, contactId,
-							MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
+							MAX_RECORD_PAYLOAD_BYTES, maxLatency);
 					setNextSendTime(db.getNextSendTime(txn, contactId));
 					db.commitTransaction(txn);
 				} finally {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/MessageFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/MessageFactoryImpl.java
index 7f92045fcbbf6984308bc0246ddd529e79d26827..8e8151079fcbd3f3a71c4ca3efd5090708ab1d67 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/MessageFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/MessageFactoryImpl.java
@@ -16,6 +16,7 @@ import static org.briarproject.bramble.api.sync.Message.FORMAT_VERSION;
 import static org.briarproject.bramble.api.sync.MessageId.BLOCK_LABEL;
 import static org.briarproject.bramble.api.sync.MessageId.ID_LABEL;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
+import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES;
 
@@ -53,6 +54,8 @@ class MessageFactoryImpl implements MessageFactory {
 	public Message createMessage(MessageId m, byte[] raw) {
 		if (raw.length < MESSAGE_HEADER_LENGTH)
 			throw new IllegalArgumentException();
+		if (raw.length > MAX_MESSAGE_LENGTH)
+			throw new IllegalArgumentException();
 		byte[] groupId = new byte[UniqueId.LENGTH];
 		System.arraycopy(raw, 0, groupId, 0, UniqueId.LENGTH);
 		long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java
index 86f8a833be9defeadebd2d2cbb05a8a2e52fcee2..b0645dc5125880882a7777cbfb188c23deb364a5 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java
@@ -29,8 +29,8 @@ import javax.annotation.concurrent.ThreadSafe;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
 
 /**
  * An outgoing {@link SyncSession} suitable for simplex transports. The session
@@ -171,7 +171,7 @@ class SimplexOutgoingSession implements SyncSession, EventListener {
 				Transaction txn = db.startTransaction(false);
 				try {
 					b = db.generateBatch(txn, contactId,
-							MAX_RECORD_PAYLOAD_LENGTH, maxLatency);
+							MAX_RECORD_PAYLOAD_BYTES, maxLatency);
 					db.commitTransaction(txn);
 				} finally {
 					db.endTransaction(txn);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncModule.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncModule.java
index 042103fbec0e0341197d4e66b5d881ab7c695dcb..0034289e7ddfcf73bcf6291264c8557079cceec2 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncModule.java
@@ -58,8 +58,9 @@ public class SyncModule {
 	}
 
 	@Provides
-	SyncRecordWriterFactory provideRecordWriterFactory() {
-		return new SyncRecordWriterFactoryImpl();
+	SyncRecordWriterFactory provideRecordWriterFactory(
+			SyncRecordWriterFactoryImpl recordWriterFactory) {
+		return recordWriterFactory;
 	}
 
 	@Provides
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderFactoryImpl.java
index 2c816d6fd7802e5ec5b1f512264e10a29ebb7266..7124f8810f20498c6af3273effe1707a7b1bde7e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderFactoryImpl.java
@@ -1,6 +1,8 @@
 package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.RecordReader;
+import org.briarproject.bramble.api.record.RecordReaderFactory;
 import org.briarproject.bramble.api.sync.MessageFactory;
 import org.briarproject.bramble.api.sync.SyncRecordReader;
 import org.briarproject.bramble.api.sync.SyncRecordReaderFactory;
@@ -15,14 +17,18 @@ import javax.inject.Inject;
 class SyncRecordReaderFactoryImpl implements SyncRecordReaderFactory {
 
 	private final MessageFactory messageFactory;
+	private final RecordReaderFactory recordReaderFactory;
 
 	@Inject
-	SyncRecordReaderFactoryImpl(MessageFactory messageFactory) {
+	SyncRecordReaderFactoryImpl(MessageFactory messageFactory,
+			RecordReaderFactory recordReaderFactory) {
 		this.messageFactory = messageFactory;
+		this.recordReaderFactory = recordReaderFactory;
 	}
 
 	@Override
 	public SyncRecordReader createRecordReader(InputStream in) {
-		return new SyncRecordReaderImpl(messageFactory, in);
+		RecordReader reader = recordReaderFactory.createRecordReader(in);
+		return new SyncRecordReaderImpl(messageFactory, reader);
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderImpl.java
index fec79b214cd3a70dc982150dc98e76d43ca96b2e..d7566979d218dd1f7c595568c60a619ce6a26685 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordReaderImpl.java
@@ -3,6 +3,8 @@ package org.briarproject.bramble.sync;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordReader;
 import org.briarproject.bramble.api.sync.Ack;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
@@ -13,72 +15,45 @@ import org.briarproject.bramble.api.sync.Request;
 import org.briarproject.bramble.api.sync.SyncRecordReader;
 import org.briarproject.bramble.util.ByteUtils;
 
+import java.io.EOFException;
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
 import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
 import static org.briarproject.bramble.api.sync.RecordTypes.MESSAGE;
 import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
 import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
-import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
 
 @NotThreadSafe
 @NotNullByDefault
 class SyncRecordReaderImpl implements SyncRecordReader {
 
-	private enum State {BUFFER_EMPTY, BUFFER_FULL, EOF}
-
 	private final MessageFactory messageFactory;
-	private final InputStream in;
-	private final byte[] header, payload;
+	private final RecordReader reader;
 
-	private State state = State.BUFFER_EMPTY;
-	private int payloadLength = 0;
+	@Nullable
+	private Record nextRecord = null;
+	private boolean eof = false;
 
-	SyncRecordReaderImpl(MessageFactory messageFactory, InputStream in) {
+	SyncRecordReaderImpl(MessageFactory messageFactory, RecordReader reader) {
 		this.messageFactory = messageFactory;
-		this.in = in;
-		header = new byte[RECORD_HEADER_LENGTH];
-		payload = new byte[MAX_RECORD_PAYLOAD_LENGTH];
+		this.reader = reader;
 	}
 
 	private void readRecord() throws IOException {
-		if (state != State.BUFFER_EMPTY) throw new IllegalStateException();
+		assert nextRecord == null;
 		while (true) {
-			// Read the header
-			int offset = 0;
-			while (offset < RECORD_HEADER_LENGTH) {
-				int read =
-						in.read(header, offset, RECORD_HEADER_LENGTH - offset);
-				if (read == -1) {
-					if (offset > 0) throw new FormatException();
-					state = State.EOF;
-					return;
-				}
-				offset += read;
-			}
-			byte version = header[0], type = header[1];
-			payloadLength = ByteUtils.readUint16(header, 2);
+			nextRecord = reader.readRecord();
 			// Check the protocol version
+			byte version = nextRecord.getProtocolVersion();
 			if (version != PROTOCOL_VERSION) throw new FormatException();
-			// Check the payload length
-			if (payloadLength > MAX_RECORD_PAYLOAD_LENGTH)
-				throw new FormatException();
-			// Read the payload
-			offset = 0;
-			while (offset < payloadLength) {
-				int read = in.read(payload, offset, payloadLength - offset);
-				if (read == -1) throw new FormatException();
-				offset += read;
-			}
-			state = State.BUFFER_FULL;
+			byte type = nextRecord.getRecordType();
 			// Return if this is a known record type, otherwise continue
 			if (type == ACK || type == MESSAGE || type == OFFER ||
 					type == REQUEST) {
@@ -87,6 +62,11 @@ class SyncRecordReaderImpl implements SyncRecordReader {
 		}
 	}
 
+	private byte getNextRecordType() {
+		assert nextRecord != null;
+		return nextRecord.getRecordType();
+	}
+
 	/**
 	 * Returns true if there's another record available or false if we've
 	 * reached the end of the input stream.
@@ -97,14 +77,21 @@ class SyncRecordReaderImpl implements SyncRecordReader {
 	 */
 	@Override
 	public boolean eof() throws IOException {
-		if (state == State.BUFFER_EMPTY) readRecord();
-		if (state == State.BUFFER_EMPTY) throw new IllegalStateException();
-		return state == State.EOF;
+		if (nextRecord != null) return false;
+		if (eof) return true;
+		try {
+			readRecord();
+			return false;
+		} catch (EOFException e) {
+			nextRecord = null;
+			eof = true;
+			return true;
+		}
 	}
 
 	@Override
 	public boolean hasAck() throws IOException {
-		return !eof() && header[1] == ACK;
+		return !eof() && getNextRecordType() == ACK;
 	}
 
 	@Override
@@ -114,27 +101,31 @@ class SyncRecordReaderImpl implements SyncRecordReader {
 	}
 
 	private List<MessageId> readMessageIds() throws IOException {
-		if (payloadLength == 0) throw new FormatException();
-		if (payloadLength % UniqueId.LENGTH != 0) throw new FormatException();
-		List<MessageId> ids = new ArrayList<>();
-		for (int off = 0; off < payloadLength; off += UniqueId.LENGTH) {
+		assert nextRecord != null;
+		byte[] payload = nextRecord.getPayload();
+		if (payload.length == 0) throw new FormatException();
+		if (payload.length % UniqueId.LENGTH != 0) throw new FormatException();
+		List<MessageId> ids = new ArrayList<>(payload.length / UniqueId.LENGTH);
+		for (int off = 0; off < payload.length; off += UniqueId.LENGTH) {
 			byte[] id = new byte[UniqueId.LENGTH];
 			System.arraycopy(payload, off, id, 0, UniqueId.LENGTH);
 			ids.add(new MessageId(id));
 		}
-		state = State.BUFFER_EMPTY;
+		nextRecord = null;
 		return ids;
 	}
 
 	@Override
 	public boolean hasMessage() throws IOException {
-		return !eof() && header[1] == MESSAGE;
+		return !eof() && getNextRecordType() == MESSAGE;
 	}
 
 	@Override
 	public Message readMessage() throws IOException {
 		if (!hasMessage()) throw new FormatException();
-		if (payloadLength <= MESSAGE_HEADER_LENGTH) throw new FormatException();
+		assert nextRecord != null;
+		byte[] payload = nextRecord.getPayload();
+		if (payload.length < MESSAGE_HEADER_LENGTH) throw new FormatException();
 		// Group ID
 		byte[] id = new byte[UniqueId.LENGTH];
 		System.arraycopy(payload, 0, id, 0, UniqueId.LENGTH);
@@ -143,16 +134,17 @@ class SyncRecordReaderImpl implements SyncRecordReader {
 		long timestamp = ByteUtils.readUint64(payload, UniqueId.LENGTH);
 		if (timestamp < 0) throw new FormatException();
 		// Body
-		byte[] body = new byte[payloadLength - MESSAGE_HEADER_LENGTH];
+		byte[] body = new byte[payload.length - MESSAGE_HEADER_LENGTH];
 		System.arraycopy(payload, MESSAGE_HEADER_LENGTH, body, 0,
-				payloadLength - MESSAGE_HEADER_LENGTH);
-		state = State.BUFFER_EMPTY;
+				payload.length - MESSAGE_HEADER_LENGTH);
+		nextRecord = null;
+		// TODO: Add a method that reuses the raw message
 		return messageFactory.createMessage(groupId, timestamp, body);
 	}
 
 	@Override
 	public boolean hasOffer() throws IOException {
-		return !eof() && header[1] == OFFER;
+		return !eof() && getNextRecordType() == OFFER;
 	}
 
 	@Override
@@ -163,7 +155,7 @@ class SyncRecordReaderImpl implements SyncRecordReader {
 
 	@Override
 	public boolean hasRequest() throws IOException {
-		return !eof() && header[1] == REQUEST;
+		return !eof() && getNextRecordType() == REQUEST;
 	}
 
 	@Override
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterFactoryImpl.java
index b715edd08a65f278c241d7388cb3e58b556e8b16..0d6a762863e3b75e0a2b28525765e75fe2c4ae18 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterFactoryImpl.java
@@ -1,16 +1,28 @@
 package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.RecordWriter;
+import org.briarproject.bramble.api.record.RecordWriterFactory;
 import org.briarproject.bramble.api.sync.SyncRecordWriter;
 import org.briarproject.bramble.api.sync.SyncRecordWriterFactory;
 
 import java.io.OutputStream;
 
+import javax.inject.Inject;
+
 @NotNullByDefault
 class SyncRecordWriterFactoryImpl implements SyncRecordWriterFactory {
 
+	private final RecordWriterFactory recordWriterFactory;
+
+	@Inject
+	SyncRecordWriterFactoryImpl(RecordWriterFactory recordWriterFactory) {
+		this.recordWriterFactory = recordWriterFactory;
+	}
+
 	@Override
 	public SyncRecordWriter createRecordWriter(OutputStream out) {
-		return new SyncRecordWriterImpl(out);
+		RecordWriter writer = recordWriterFactory.createRecordWriter(out);
+		return new SyncRecordWriterImpl(writer);
 	}
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterImpl.java
index 6c41945c6a1b059e4bbd2e204f11431efda227d9..d5e59f78dfbd03d439080e7499ad611212cbf394 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SyncRecordWriterImpl.java
@@ -1,81 +1,67 @@
 package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordWriter;
 import org.briarproject.bramble.api.sync.Ack;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.Offer;
-import org.briarproject.bramble.api.sync.RecordTypes;
 import org.briarproject.bramble.api.sync.Request;
 import org.briarproject.bramble.api.sync.SyncRecordWriter;
-import org.briarproject.bramble.util.ByteUtils;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
 
 import javax.annotation.concurrent.NotThreadSafe;
 
 import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
+import static org.briarproject.bramble.api.sync.RecordTypes.MESSAGE;
 import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
 import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
-import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
 
 @NotThreadSafe
 @NotNullByDefault
 class SyncRecordWriterImpl implements SyncRecordWriter {
 
-	private final OutputStream out;
-	private final byte[] header;
-	private final ByteArrayOutputStream payload;
+	private final RecordWriter writer;
+	private final ByteArrayOutputStream payload = new ByteArrayOutputStream();
 
-	SyncRecordWriterImpl(OutputStream out) {
-		this.out = out;
-		header = new byte[RECORD_HEADER_LENGTH];
-		header[0] = PROTOCOL_VERSION;
-		payload = new ByteArrayOutputStream(MAX_RECORD_PAYLOAD_LENGTH);
+	SyncRecordWriterImpl(RecordWriter writer) {
+		this.writer = writer;
 	}
 
 	private void writeRecord(byte recordType) throws IOException {
-		header[1] = recordType;
-		ByteUtils.writeUint16(payload.size(), header, 2);
-		out.write(header);
-		payload.writeTo(out);
+		writer.writeRecord(new Record(PROTOCOL_VERSION, recordType,
+				payload.toByteArray()));
 		payload.reset();
 	}
 
 	@Override
 	public void writeAck(Ack a) throws IOException {
-		if (payload.size() != 0) throw new IllegalStateException();
 		for (MessageId m : a.getMessageIds()) payload.write(m.getBytes());
 		writeRecord(ACK);
 	}
 
 	@Override
 	public void writeMessage(byte[] raw) throws IOException {
-		header[1] = RecordTypes.MESSAGE;
-		ByteUtils.writeUint16(raw.length, header, 2);
-		out.write(header);
-		out.write(raw);
+		writer.writeRecord(new Record(PROTOCOL_VERSION, MESSAGE, raw));
 	}
 
 	@Override
 	public void writeOffer(Offer o) throws IOException {
-		if (payload.size() != 0) throw new IllegalStateException();
 		for (MessageId m : o.getMessageIds()) payload.write(m.getBytes());
 		writeRecord(OFFER);
 	}
 
 	@Override
 	public void writeRequest(Request r) throws IOException {
-		if (payload.size() != 0) throw new IllegalStateException();
 		for (MessageId m : r.getMessageIds()) payload.write(m.getBytes());
 		writeRecord(REQUEST);
 	}
 
 	@Override
 	public void flush() throws IOException {
-		out.flush();
+		writer.flush();
 	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
index 00fa90f434f2718e73fd8c4e73a3d1612a5c31bd..1504da9eb48cfcd038d724c04c4fa5398e8d954e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
@@ -112,8 +112,8 @@ public class SyncIntegrationTest extends BrambleTestCase {
 		recordWriter.writeMessage(message1.getRaw());
 		recordWriter.writeOffer(new Offer(messageIds));
 		recordWriter.writeRequest(new Request(messageIds));
+		recordWriter.flush();
 
-		streamWriter.flush();
 		return out.toByteArray();
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
index a714520dd5df088f882a160d33b3d7c6a48be117..0397f549fd72b8291396c3d4a0d8f7d03212b2d5 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTestComponent.java
@@ -1,6 +1,7 @@
 package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.crypto.CryptoModule;
+import org.briarproject.bramble.record.RecordModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestSecureRandomModule;
 import org.briarproject.bramble.transport.TransportModule;
@@ -13,6 +14,7 @@ import dagger.Component;
 @Component(modules = {
 		TestSecureRandomModule.class,
 		CryptoModule.class,
+		RecordModule.class,
 		SyncModule.class,
 		SystemModule.class,
 		TransportModule.class
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncRecordReaderImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncRecordReaderImplTest.java
index 4b46a12988018f4fc0658b3a07bfa58f808c340a..ccd1207ffa0e2c60457fea5fccbb4da97c37e13e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncRecordReaderImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncRecordReaderImplTest.java
@@ -2,23 +2,28 @@ package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.UniqueId;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordReader;
 import org.briarproject.bramble.api.sync.Ack;
 import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.Offer;
+import org.briarproject.bramble.api.sync.Request;
+import org.briarproject.bramble.api.sync.SyncRecordReader;
 import org.briarproject.bramble.test.BrambleMockTestCase;
-import org.briarproject.bramble.test.TestUtils;
-import org.briarproject.bramble.util.ByteUtils;
+import org.jmock.Expectations;
 import org.junit.Test;
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
 
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
 import static org.briarproject.bramble.api.sync.RecordTypes.ACK;
 import static org.briarproject.bramble.api.sync.RecordTypes.OFFER;
 import static org.briarproject.bramble.api.sync.RecordTypes.REQUEST;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_IDS;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.PROTOCOL_VERSION;
-import static org.briarproject.bramble.api.sync.SyncConstants.RECORD_HEADER_LENGTH;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -27,209 +32,164 @@ public class SyncRecordReaderImplTest extends BrambleMockTestCase {
 
 	private final MessageFactory messageFactory =
 			context.mock(MessageFactory.class);
-
-	@Test(expected = FormatException.class)
-	public void testFormatExceptionIfAckIsTooLarge() throws Exception {
-		byte[] b = createAck(true);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readAck();
-	}
+	private final RecordReader recordReader = context.mock(RecordReader.class);
 
 	@Test
 	public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception {
-		byte[] b = createAck(false);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readAck();
+		expectReadRecord(createAck());
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		Ack ack = reader.readAck();
+		assertEquals(MAX_MESSAGE_IDS, ack.getMessageIds().size());
 	}
 
 	@Test(expected = FormatException.class)
 	public void testFormatExceptionIfAckIsEmpty() throws Exception {
-		byte[] b = createEmptyAck();
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readAck();
-	}
+		expectReadRecord(createEmptyAck());
 
-	@Test(expected = FormatException.class)
-	public void testFormatExceptionIfOfferIsTooLarge() throws Exception {
-		byte[] b = createOffer(true);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readOffer();
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		reader.readAck();
 	}
 
 	@Test
 	public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception {
-		byte[] b = createOffer(false);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readOffer();
+		expectReadRecord(createOffer());
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		Offer offer = reader.readOffer();
+		assertEquals(MAX_MESSAGE_IDS, offer.getMessageIds().size());
 	}
 
 	@Test(expected = FormatException.class)
 	public void testFormatExceptionIfOfferIsEmpty() throws Exception {
-		byte[] b = createEmptyOffer();
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readOffer();
-	}
+		expectReadRecord(createEmptyOffer());
 
-	@Test(expected = FormatException.class)
-	public void testFormatExceptionIfRequestIsTooLarge() throws Exception {
-		byte[] b = createRequest(true);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readRequest();
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		reader.readOffer();
 	}
 
 	@Test
 	public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception {
-		byte[] b = createRequest(false);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.readRequest();
+		expectReadRecord(createRequest());
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		Request request = reader.readRequest();
+		assertEquals(MAX_MESSAGE_IDS, request.getMessageIds().size());
 	}
 
 	@Test(expected = FormatException.class)
 	public void testFormatExceptionIfRequestIsEmpty() throws Exception {
-		byte[] b = createEmptyRequest();
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
+		expectReadRecord(createEmptyRequest());
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
 		reader.readRequest();
 	}
 
 	@Test
 	public void testEofReturnsTrueWhenAtEndOfStream() throws Exception {
-		ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
+		context.checking(new Expectations() {{
+			oneOf(recordReader).readRecord();
+			will(throwException(new EOFException()));
+		}});
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		assertTrue(reader.eof());
 		assertTrue(reader.eof());
 	}
 
 	@Test
 	public void testEofReturnsFalseWhenNotAtEndOfStream() throws Exception {
-		byte[] b = createAck(false);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		assertFalse(reader.eof());
-	}
-
-	@Test(expected = FormatException.class)
-	public void testThrowsExceptionIfHeaderIsTooShort() throws Exception {
-		byte[] b = new byte[RECORD_HEADER_LENGTH - 1];
-		b[0] = PROTOCOL_VERSION;
-		b[1] = ACK;
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.eof();
-	}
+		expectReadRecord(createAck());
 
-	@Test(expected = FormatException.class)
-	public void testThrowsExceptionIfPayloadIsTooShort() throws Exception {
-		int payloadLength = 123;
-		byte[] b = new byte[RECORD_HEADER_LENGTH + payloadLength - 1];
-		b[0] = PROTOCOL_VERSION;
-		b[1] = ACK;
-		ByteUtils.writeUint16(payloadLength, b, 2);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.eof();
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
+		assertFalse(reader.eof());
+		assertFalse(reader.eof());
 	}
 
 	@Test(expected = FormatException.class)
 	public void testThrowsExceptionIfProtocolVersionIsUnrecognised()
 			throws Exception {
 		byte version = (byte) (PROTOCOL_VERSION + 1);
-		byte[] b = createRecord(version, ACK, new byte[0]);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
-		reader.eof();
-	}
+		byte[] payload = getRandomId();
 
-	@Test(expected = FormatException.class)
-	public void testThrowsExceptionIfPayloadIsTooLong() throws Exception {
-		byte[] payload = new byte[MAX_RECORD_PAYLOAD_LENGTH + 1];
-		byte[] b = createRecord(PROTOCOL_VERSION, ACK, payload);
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
+		expectReadRecord(new Record(version, ACK, payload));
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
 		reader.eof();
 	}
 
 	@Test
 	public void testSkipsUnrecognisedRecordTypes() throws Exception {
-		byte[] skip1 = createRecord(PROTOCOL_VERSION, (byte) (REQUEST + 1),
-				new byte[123]);
-		byte[] skip2 = createRecord(PROTOCOL_VERSION, (byte) (REQUEST + 2),
-				new byte[0]);
-		byte[] ack = createAck(false);
-		ByteArrayOutputStream input = new ByteArrayOutputStream();
-		input.write(skip1);
-		input.write(skip2);
-		input.write(ack);
-		ByteArrayInputStream in = new ByteArrayInputStream(input.toByteArray());
-		SyncRecordReaderImpl
-				reader = new SyncRecordReaderImpl(messageFactory, in);
+		byte type1 = (byte) (REQUEST + 1);
+		byte[] payload1 = getRandomBytes(123);
+		Record unknownRecord1 = new Record(PROTOCOL_VERSION, type1, payload1);
+		byte type2 = (byte) (REQUEST + 2);
+		byte[] payload2 = new byte[0];
+		Record unknownRecord2 = new Record(PROTOCOL_VERSION, type2, payload2);
+		Record ackRecord = createAck();
+
+		context.checking(new Expectations() {{
+			oneOf(recordReader).readRecord();
+			will(returnValue(unknownRecord1));
+			oneOf(recordReader).readRecord();
+			will(returnValue(unknownRecord2));
+			oneOf(recordReader).readRecord();
+			will(returnValue(ackRecord));
+
+		}});
+
+		SyncRecordReader reader =
+				new SyncRecordReaderImpl(messageFactory, recordReader);
 		assertTrue(reader.hasAck());
 		Ack a = reader.readAck();
 		assertEquals(MAX_MESSAGE_IDS, a.getMessageIds().size());
 	}
 
-	private byte[] createAck(boolean tooBig) throws Exception {
-		return createRecord(PROTOCOL_VERSION, ACK, createPayload(tooBig));
+	private void expectReadRecord(Record record) throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(recordReader).readRecord();
+			will(returnValue(record));
+		}});
 	}
 
-	private byte[] createEmptyAck() throws Exception {
-		return createRecord(PROTOCOL_VERSION, ACK, new byte[0]);
+	private Record createAck() throws Exception {
+		return new Record(PROTOCOL_VERSION, ACK, createPayload());
 	}
 
-	private byte[] createOffer(boolean tooBig) throws Exception {
-		return createRecord(PROTOCOL_VERSION, OFFER, createPayload(tooBig));
+	private Record createEmptyAck() throws Exception {
+		return new Record(PROTOCOL_VERSION, ACK, new byte[0]);
 	}
 
-	private byte[] createEmptyOffer() throws Exception {
-		return createRecord(PROTOCOL_VERSION, OFFER, new byte[0]);
+	private Record createOffer() throws Exception {
+		return new Record(PROTOCOL_VERSION, OFFER, createPayload());
 	}
 
-	private byte[] createRequest(boolean tooBig) throws Exception {
-		return createRecord(PROTOCOL_VERSION, REQUEST, createPayload(tooBig));
+	private Record createEmptyOffer() throws Exception {
+		return new Record(PROTOCOL_VERSION, OFFER, new byte[0]);
 	}
 
-	private byte[] createEmptyRequest() throws Exception {
-		return createRecord(PROTOCOL_VERSION, REQUEST, new byte[0]);
+	private Record createRequest() throws Exception {
+		return new Record(PROTOCOL_VERSION, REQUEST, createPayload());
 	}
 
-	private byte[] createRecord(byte version, byte type, byte[] payload) {
-		byte[] b = new byte[RECORD_HEADER_LENGTH + payload.length];
-		b[0] = version;
-		b[1] = type;
-		ByteUtils.writeUint16(payload.length, b, 2);
-		System.arraycopy(payload, 0, b, RECORD_HEADER_LENGTH, payload.length);
-		return b;
+	private Record createEmptyRequest() throws Exception {
+		return new Record(PROTOCOL_VERSION, REQUEST, new byte[0]);
 	}
 
-	private byte[] createPayload(boolean tooBig) throws Exception {
+	private byte[] createPayload() throws Exception {
 		ByteArrayOutputStream payload = new ByteArrayOutputStream();
-		while (payload.size() + UniqueId.LENGTH <= MAX_RECORD_PAYLOAD_LENGTH) {
-			payload.write(TestUtils.getRandomId());
+		while (payload.size() + UniqueId.LENGTH <= MAX_RECORD_PAYLOAD_BYTES) {
+			payload.write(getRandomId());
 		}
-		if (tooBig) payload.write(TestUtils.getRandomId());
-		assertEquals(tooBig, payload.size() > MAX_RECORD_PAYLOAD_LENGTH);
 		return payload.toByteArray();
 	}
 }
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index 160d80e534c9d3b234835ad0f08b8293c4724056..20a59f21a1f4cb0a0aea7a4f82c00025da6e869d 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -10,6 +10,7 @@ import org.briarproject.bramble.event.EventModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
 import org.briarproject.bramble.properties.PropertiesModule;
+import org.briarproject.bramble.record.RecordModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
@@ -52,6 +53,7 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		PropertiesModule.class,
+		RecordModule.class,
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
index c5a1f1851cb99e3acf668e9d400cd6d031e465c1..9e828d722ddae2caa5dd137c800057f4e81dc90c 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/MessageSizeIntegrationTest.java
@@ -26,7 +26,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_RECORD_PAYLOAD_LENGTH;
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
@@ -63,7 +63,7 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
 		int length = message.getMessage().getRaw().length;
 		assertTrue(
 				length > UniqueId.LENGTH + 8 + MAX_PRIVATE_MESSAGE_BODY_LENGTH);
-		assertTrue(length <= MAX_RECORD_PAYLOAD_LENGTH);
+		assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES);
 	}
 
 	@Test
@@ -87,7 +87,7 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
 		assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH + 4
 				+ MAX_AUTHOR_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH
 				+ MAX_FORUM_POST_BODY_LENGTH);
-		assertTrue(length <= MAX_RECORD_PAYLOAD_LENGTH);
+		assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES);
 	}
 
 	private static void injectEagerSingletons(
diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
index f3d6f039977361459b26333f5ade3d6538db3657..7dade1ed198f8223971bf55255ed03ecb2a79ff7 100644
--- a/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/messaging/SimplexMessagingIntegrationTestComponent.java
@@ -16,6 +16,7 @@ import org.briarproject.bramble.db.DatabaseModule;
 import org.briarproject.bramble.event.EventModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
+import org.briarproject.bramble.record.RecordModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestCryptoExecutorModule;
@@ -48,6 +49,7 @@ import dagger.Component;
 		IdentityModule.class,
 		LifecycleModule.class,
 		MessagingModule.class,
+		RecordModule.class,
 		SyncModule.class,
 		SystemModule.class,
 		TransportModule.class,
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index 131f92ea8b8b910ab7f49ca9f00466900fd9c15c..9d86e58d97f841cf01cd1339f8749bc71eeb448f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -19,6 +19,7 @@ import org.briarproject.bramble.event.EventModule;
 import org.briarproject.bramble.identity.IdentityModule;
 import org.briarproject.bramble.lifecycle.LifecycleModule;
 import org.briarproject.bramble.properties.PropertiesModule;
+import org.briarproject.bramble.record.RecordModule;
 import org.briarproject.bramble.sync.SyncModule;
 import org.briarproject.bramble.system.SystemModule;
 import org.briarproject.bramble.test.TestDatabaseModule;
@@ -73,6 +74,7 @@ import dagger.Component;
 		MessagingModule.class,
 		PrivateGroupModule.class,
 		PropertiesModule.class,
+		RecordModule.class,
 		SharingModule.class,
 		SyncModule.class,
 		SystemModule.class,