diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/record/Record.java b/bramble-api/src/main/java/org/briarproject/bramble/api/record/Record.java
new file mode 100644
index 0000000000000000000000000000000000000000..fedb7212c2ad75dd35fea5b7e1799a2b81022f67
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/record/Record.java
@@ -0,0 +1,36 @@
+package org.briarproject.bramble.api.record;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class Record {
+
+	public static final int RECORD_HEADER_BYTES = 4;
+	public static final int MAX_RECORD_PAYLOAD_BYTES = 48 * 1024; // 48 KiB
+
+	private final byte protocolVersion, recordType;
+	private final byte[] payload;
+
+	public Record(byte protocolVersion, byte recordType, byte[] payload) {
+		if (payload.length > MAX_RECORD_PAYLOAD_BYTES)
+			throw new IllegalArgumentException();
+		this.protocolVersion = protocolVersion;
+		this.recordType = recordType;
+		this.payload = payload;
+	}
+
+	public byte getProtocolVersion() {
+		return protocolVersion;
+	}
+
+	public byte getRecordType() {
+		return recordType;
+	}
+
+	public byte[] getPayload() {
+		return payload;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReader.java b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..371dead2025f40a1dc5e4fc8f7f6b15d5f76cc45
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReader.java
@@ -0,0 +1,20 @@
+package org.briarproject.bramble.api.record;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+@NotNullByDefault
+public interface RecordReader {
+
+	/**
+	 * Reads and returns the next record.
+	 *
+	 * @throws EOFException if the end of the stream is reached without reading
+	 * a complete record
+	 */
+	Record readRecord() throws IOException;
+
+	void close() throws IOException;
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReaderFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReaderFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..17588eaed2d17871b86b8f15de8b7847a97b01d7
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordReaderFactory.java
@@ -0,0 +1,8 @@
+package org.briarproject.bramble.api.record;
+
+import java.io.InputStream;
+
+public interface RecordReaderFactory {
+
+	RecordReader createRecordReader(InputStream in);
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriter.java b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb83d4d410ae358e2978f8557980cc82600b0a57
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriter.java
@@ -0,0 +1,15 @@
+package org.briarproject.bramble.api.record;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.io.IOException;
+
+@NotNullByDefault
+public interface RecordWriter {
+
+	void writeRecord(Record r) throws IOException;
+
+	void flush() throws IOException;
+
+	void close() throws IOException;
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriterFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..2713bb4febad77a8363ca2286782b2b44d457547
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/record/RecordWriterFactory.java
@@ -0,0 +1,8 @@
+package org.briarproject.bramble.api.record;
+
+import java.io.OutputStream;
+
+public interface RecordWriterFactory {
+
+	RecordWriter createRecordWriter(OutputStream out);
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/record/RecordModule.java b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..54dd4587c3285be5f575051faf89fc6af5aa1212
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordModule.java
@@ -0,0 +1,21 @@
+package org.briarproject.bramble.record;
+
+import org.briarproject.bramble.api.record.RecordReaderFactory;
+import org.briarproject.bramble.api.record.RecordWriterFactory;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class RecordModule {
+
+	@Provides
+	RecordReaderFactory provideRecordReaderFactory() {
+		return new RecordReaderFactoryImpl();
+	}
+
+	@Provides
+	RecordWriterFactory provideRecordWriterFactory() {
+		return new RecordWriterFactoryImpl();
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8b89d8883a5cb601627dc4df4c790791a9803ce
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderFactoryImpl.java
@@ -0,0 +1,14 @@
+package org.briarproject.bramble.record;
+
+import org.briarproject.bramble.api.record.RecordReader;
+import org.briarproject.bramble.api.record.RecordReaderFactory;
+
+import java.io.InputStream;
+
+class RecordReaderFactoryImpl implements RecordReaderFactory {
+
+	@Override
+	public RecordReader createRecordReader(InputStream in) {
+		return new RecordReaderImpl(in);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..92fb696ae54eae284083b10a2dcf37864ed206dd
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordReaderImpl.java
@@ -0,0 +1,43 @@
+package org.briarproject.bramble.record;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordReader;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
+
+@NotThreadSafe
+@NotNullByDefault
+class RecordReaderImpl implements RecordReader {
+
+	private final DataInputStream in;
+
+	RecordReaderImpl(InputStream in) {
+		this.in = new DataInputStream(new BufferedInputStream(in, 1024));
+	}
+
+	@Override
+	public Record readRecord() throws IOException {
+		byte protocolVersion = in.readByte();
+		byte recordType = in.readByte();
+		int payloadLength = in.readShort() & 0xFFFF; // Convert to unsigned
+		if (payloadLength < 0 || payloadLength > MAX_RECORD_PAYLOAD_BYTES)
+			throw new FormatException();
+		byte[] payload = new byte[payloadLength];
+		in.readFully(payload);
+		return new Record(protocolVersion, recordType, payload);
+	}
+
+	@Override
+	public void close() throws IOException {
+		in.close();
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..a20742423478c335e1708acdccdf54ad4bf16cc2
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterFactoryImpl.java
@@ -0,0 +1,14 @@
+package org.briarproject.bramble.record;
+
+import org.briarproject.bramble.api.record.RecordWriter;
+import org.briarproject.bramble.api.record.RecordWriterFactory;
+
+import java.io.OutputStream;
+
+class RecordWriterFactoryImpl implements RecordWriterFactory {
+
+	@Override
+	public RecordWriter createRecordWriter(OutputStream out) {
+		return new RecordWriterImpl(out);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..281b3c6382c1257e48140dbdaa83c3afe168a461
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/record/RecordWriterImpl.java
@@ -0,0 +1,42 @@
+package org.briarproject.bramble.record;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.record.Record;
+import org.briarproject.bramble.api.record.RecordWriter;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+class RecordWriterImpl implements RecordWriter {
+
+	private final DataOutputStream out;
+
+	RecordWriterImpl(OutputStream out) {
+		this.out = new DataOutputStream(new BufferedOutputStream(out, 1024));
+	}
+
+	@Override
+	public void writeRecord(Record r) throws IOException {
+		out.write(r.getProtocolVersion());
+		out.write(r.getRecordType());
+		byte[] payload = r.getPayload();
+		out.writeShort(payload.length);
+		out.write(payload);
+	}
+
+	@Override
+	public void flush() throws IOException {
+		out.flush();
+	}
+
+	@Override
+	public void close() throws IOException {
+		out.close();
+	}
+}