diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 0f541d6fb34dd2af859df89fb23e7675fff52349..b1652f5fb7c1ea51c54367e5d0137ac4bdc99d83 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -75,6 +75,7 @@
 	<string name="send">Send</string>
 	<string name="no_data">No data</string>
 	<string name="ellipsis">…</string>
+	<string name="text_too_long">The entered text is too long</string>
 
 	<!-- Contacts and Private Conversations-->
 	<string name="no_contacts">It seems that you are new here and have no contacts yet.\n\nTap the + icon at the top and follow the instructions to add some friends to your list.\n\nPlease remember: You can only add new contacts face-to-face to prevent anyone from impersonating you or reading your messages in the future.</string>
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 74537eed76d8c4ee4fc16948803d96c1d7853a61..eafa66098ca47026d91cf04be40e47c756145e49 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -2,7 +2,9 @@ package org.briarproject.android.forum;
 
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.os.Bundle;
 import android.support.annotation.LayoutRes;
+import android.support.annotation.StringRes;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.ActivityOptionsCompat;
 import android.support.v7.app.AlertDialog;
@@ -29,6 +31,7 @@ import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
 import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
 import static android.widget.Toast.LENGTH_SHORT;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
 
 public class ForumActivity extends
 		ThreadListActivity<Forum, ForumEntry, ForumPostHeader, NestedForumAdapter> {
@@ -36,7 +39,7 @@ public class ForumActivity extends
 	private static final int REQUEST_FORUM_SHARED = 3;
 
 	@Inject
-	protected ForumController forumController;
+	ForumController forumController;
 
 	@Override
 	public void injectActivity(ActivityComponent component) {
@@ -49,7 +52,23 @@ public class ForumActivity extends
 	}
 
 	@Override
-	protected @LayoutRes int getLayout() {
+	public void onCreate(Bundle state) {
+		super.onCreate(state);
+
+		Intent i = getIntent();
+		String groupName = i.getStringExtra(GROUP_NAME);
+		if (groupName != null) setTitle(groupName);
+		else loadNamedGroup();
+	}
+
+	@Override
+	protected void onNamedGroupLoaded(Forum forum) {
+		setTitle(forum.getName());
+	}
+
+	@Override
+	@LayoutRes
+	protected int getLayout() {
 		return R.layout.activity_forum;
 	}
 
@@ -110,11 +129,18 @@ public class ForumActivity extends
 	}
 
 	@Override
+	protected int getMaxBodyLength() {
+		return MAX_FORUM_POST_BODY_LENGTH;
+	}
+
+	@Override
+	@StringRes
 	protected int getItemPostedString() {
 		return R.string.forum_new_entry_posted;
 	}
 
 	@Override
+	@StringRes
 	protected int getItemReceivedString() {
 		return R.string.forum_new_entry_received;
 	}
@@ -125,24 +151,7 @@ public class ForumActivity extends
 					@Override
 					public void onClick(final DialogInterface dialog,
 							int which) {
-						forumController.deleteGroupItem(
-								new UiResultExceptionHandler<Void, DbException>(
-										ForumActivity.this) {
-									@Override
-									public void onResultUi(Void v) {
-										Toast.makeText(ForumActivity.this,
-												R.string.forum_left_toast,
-												LENGTH_SHORT)
-												.show();
-									}
-
-									@Override
-									public void onExceptionUi(
-											DbException exception) {
-										// TODO proper error handling
-										dialog.dismiss();
-									}
-								});
+						deleteNamedGroup();
 					}
 				};
 		AlertDialog.Builder builder =
@@ -155,4 +164,25 @@ public class ForumActivity extends
 		builder.show();
 	}
 
+	private void deleteNamedGroup() {
+		forumController.deleteNamedGroup(
+				new UiResultExceptionHandler<Void, DbException>(
+						ForumActivity.this) {
+					@Override
+					public void onResultUi(Void v) {
+						Toast.makeText(ForumActivity.this,
+								R.string.forum_left_toast,
+								LENGTH_SHORT)
+								.show();
+					}
+
+					@Override
+					public void onExceptionUi(
+							DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index 9375d0a35d40ab6b81245b784c4e8f555b4bd6ff..9ec21e76cfa49adf1da8d1a7b64171b3b6451821 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -15,11 +15,12 @@ import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -48,7 +49,7 @@ public class ForumControllerImpl
 	@Override
 	public void onActivityResume() {
 		super.onActivityResume();
-		notificationManager.clearForumPostNotification(groupId);
+		notificationManager.clearForumPostNotification(getGroupId());
 	}
 
 	@Override
@@ -57,7 +58,7 @@ public class ForumControllerImpl
 
 		if (e instanceof ForumPostReceivedEvent) {
 			final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
-			if (pe.getGroupId().equals(groupId)) {
+			if (pe.getGroupId().equals(getGroupId())) {
 				LOG.info("Forum post received, adding...");
 				final ForumPostHeader fph = pe.getForumPostHeader();
 				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@@ -72,35 +73,38 @@ public class ForumControllerImpl
 
 	@Override
 	protected Forum loadGroupItem() throws DbException {
-		return forumManager.getForum(groupId);
+		return forumManager.getForum(getGroupId());
 	}
 
 	@Override
 	protected Collection<ForumPostHeader> loadHeaders() throws DbException {
-		return forumManager.getPostHeaders(groupId);
+		return forumManager.getPostHeaders(getGroupId());
 	}
 
 	@Override
-	protected void loadBodies(Collection<ForumPostHeader> headers)
+	protected Map<MessageId, String> loadBodies(
+			Collection<ForumPostHeader> headers)
 			throws DbException {
+		Map<MessageId, String> bodies = new HashMap<>();
 		for (ForumPostHeader header : headers) {
 			if (!bodyCache.containsKey(header.getId())) {
 				String body = StringUtils
 						.fromUtf8(forumManager.getPostBody(header.getId()));
-				bodyCache.put(header.getId(), body);
+				bodies.put(header.getId(), body);
 			}
 		}
+		return bodies;
 	}
 
 	@Override
 	protected void markRead(MessageId id) throws DbException {
-		forumManager.setReadFlag(groupId, id, true);
+		forumManager.setReadFlag(getGroupId(), id, true);
 	}
 
 	@Override
-	protected ForumPost createLocalMessage(GroupId g, String body,
+	protected ForumPost createLocalMessage(String body,
 			@Nullable MessageId parentId) throws DbException {
-		return forumManager.createLocalPost(groupId, body, parentId);
+		return forumManager.createLocalPost(getGroupId(), body, parentId);
 	}
 
 	@Override
@@ -115,8 +119,8 @@ public class ForumControllerImpl
 	}
 
 	@Override
-	protected ForumEntry buildItem(ForumPostHeader header) {
-		return new ForumEntry(header, bodyCache.get(header.getId()));
+	protected ForumEntry buildItem(ForumPostHeader header, String body) {
+		return new ForumEntry(header, body);
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
index 0ad58273464064702ddee40a6cb7b7d5a3b7fe80..71a4865b516bf1e14e8b5d1ef9c6317179092c2a 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
@@ -1,8 +1,9 @@
 package org.briarproject.android.privategroup.conversation;
 
+import android.content.Intent;
 import android.os.Bundle;
 import android.support.annotation.LayoutRes;
-import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.LinearLayoutManager;
 import android.view.Menu;
@@ -18,11 +19,13 @@ import org.briarproject.api.privategroup.PrivateGroup;
 
 import javax.inject.Inject;
 
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
+
 public class GroupActivity extends
 		ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> {
 
 	@Inject
-	protected GroupController controller;
+	GroupController controller;
 
 	@Override
 	public void injectActivity(ActivityComponent component) {
@@ -37,23 +40,18 @@ public class GroupActivity extends
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
-		list.setEmptyText(R.string.groups_no_messages);
-	}
 
-	@Override
-	protected @LayoutRes int getLayout() {
-		return R.layout.activity_forum;
-	}
+		Intent i = getIntent();
+		String groupName = i.getStringExtra(GROUP_NAME);
+		if (groupName != null) setTitle(groupName);
+		loadNamedGroup();
 
-	@Override
-	protected void setActionBarTitle(@Nullable String title) {
-		if (title != null) setTitle(title);
-		loadGroupItem();
+		list.setEmptyText(R.string.groups_no_messages);
 	}
 
 	@Override
-	protected void onGroupItemLoaded(PrivateGroup group) {
-		super.onGroupItemLoaded(group);
+	protected void onNamedGroupLoaded(PrivateGroup group) {
+		setTitle(group.getName());
 		// Created by
 		ActionBar actionBar = getSupportActionBar();
 		if (actionBar != null) {
@@ -62,6 +60,12 @@ public class GroupActivity extends
 		}
 	}
 
+	@Override
+	@LayoutRes
+	protected int getLayout() {
+		return R.layout.activity_forum;
+	}
+
 	@Override
 	protected GroupMessageAdapter createAdapter(
 			LinearLayoutManager layoutManager) {
@@ -89,11 +93,18 @@ public class GroupActivity extends
 	}
 
 	@Override
+	protected int getMaxBodyLength() {
+		return MAX_GROUP_POST_BODY_LENGTH;
+	}
+
+	@Override
+	@StringRes
 	protected int getItemPostedString() {
 		return R.string.groups_message_sent;
 	}
 
 	@Override
+	@StringRes
 	protected int getItemReceivedString() {
 		return R.string.groups_message_received;
 	}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
index 18a9bb57e357bb6945626a2e7a64f14e6299d0fc..dfadc9d414f846b91f6e29435f9444d19703b10a 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
@@ -15,10 +15,11 @@ import org.briarproject.api.privategroup.GroupMessage;
 import org.briarproject.api.privategroup.GroupMessageHeader;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupManager;
-import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -47,7 +48,7 @@ public class GroupControllerImpl
 	@Override
 	public void onActivityResume() {
 		super.onActivityResume();
-		notificationManager.clearForumPostNotification(groupId);
+		notificationManager.clearForumPostNotification(getGroupId());
 	}
 
 	@Override
@@ -55,14 +56,14 @@ public class GroupControllerImpl
 		super.eventOccurred(e);
 
 		if (e instanceof GroupMessageAddedEvent) {
-			final GroupMessageAddedEvent pe = (GroupMessageAddedEvent) e;
-			if (!pe.isLocal() && pe.getGroupId().equals(groupId)) {
+			final GroupMessageAddedEvent gmae = (GroupMessageAddedEvent) e;
+			if (!gmae.isLocal() && gmae.getGroupId().equals(getGroupId())) {
 				LOG.info("Group message received, adding...");
-				final GroupMessageHeader fph = pe.getHeader();
+				final GroupMessageHeader h = gmae.getHeader();
 				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
 					@Override
 					public void run() {
-						listener.onHeaderReceived(fph);
+						listener.onHeaderReceived(h);
 					}
 				});
 			}
@@ -71,35 +72,39 @@ public class GroupControllerImpl
 
 	@Override
 	protected PrivateGroup loadGroupItem() throws DbException {
-		return privateGroupManager.getPrivateGroup(groupId);
+		return privateGroupManager.getPrivateGroup(getGroupId());
 	}
 
 	@Override
 	protected Collection<GroupMessageHeader> loadHeaders() throws DbException {
-		return privateGroupManager.getHeaders(groupId);
+		return privateGroupManager.getHeaders(getGroupId());
 	}
 
 	@Override
-	protected void loadBodies(Collection<GroupMessageHeader> headers)
+	protected Map<MessageId, String> loadBodies(
+			Collection<GroupMessageHeader> headers)
 			throws DbException {
+		Map<MessageId, String> bodies = new HashMap<>();
 		for (GroupMessageHeader header : headers) {
 			if (!bodyCache.containsKey(header.getId())) {
 				String body =
 						privateGroupManager.getMessageBody(header.getId());
-				bodyCache.put(header.getId(), body);
+				bodies.put(header.getId(), body);
 			}
 		}
+		return bodies;
 	}
 
 	@Override
 	protected void markRead(MessageId id) throws DbException {
-		privateGroupManager.setReadFlag(groupId, id, true);
+		privateGroupManager.setReadFlag(getGroupId(), id, true);
 	}
 
 	@Override
-	protected GroupMessage createLocalMessage(GroupId g, String body,
+	protected GroupMessage createLocalMessage(String body,
 			@Nullable MessageId parentId) throws DbException {
-		return privateGroupManager.createLocalMessage(groupId, body, parentId);
+		return privateGroupManager
+				.createLocalMessage(getGroupId(), body, parentId);
 	}
 
 	@Override
@@ -114,8 +119,9 @@ public class GroupControllerImpl
 	}
 
 	@Override
-	protected GroupMessageItem buildItem(GroupMessageHeader header) {
-		return new GroupMessageItem(header, bodyCache.get(header.getId()));
+	protected GroupMessageItem buildItem(GroupMessageHeader header,
+			String body) {
+		return new GroupMessageItem(header, body);
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
index 82e1fca18f4873f658c4f4b0f0559093384b9a51..01aae5c68b8f50689b88759e1ba8ee43d71d2a86 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
@@ -41,7 +41,7 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
 
 	@Override
 	public void onBindViewHolder(ThreadItemViewHolder<I> ui, int position) {
-		final I item = getVisibleItem(position);
+		I item = getVisibleItem(position);
 		if (item == null) return;
 		listener.onItemVisible(item);
 		ui.bind(this, listener, item, position);
@@ -151,10 +151,9 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
 		}
 	}
 
-	public void setReplyItemById(byte[] id) {
-		MessageId messageId = new MessageId(id);
+	public void setReplyItemById(MessageId id) {
 		for (I item : items) {
-			if (item.getId().equals(messageId)) {
+			if (item.getId().equals(id)) {
 				setReplyItem(item);
 				break;
 			}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
index 529d588de0f5d54e7fa24199aa21c2f382888774..ab480a4bd04f84f0cc51517b6412d6e81518191a 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
@@ -21,10 +21,11 @@ import org.briarproject.android.threaded.ThreadListController.ThreadListListener
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.android.view.TextInputView;
 import org.briarproject.android.view.TextInputView.TextInputListener;
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -34,7 +35,7 @@ import static android.support.design.widget.Snackbar.make;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
-public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
+public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
 		extends BriarActivity
 		implements ThreadListListener<H>, TextInputListener,
 		ThreadItemListener<I> {
@@ -46,7 +47,7 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 	protected BriarRecyclerView list;
 	protected TextInputView textInput;
 	protected GroupId groupId;
-	private byte[] replyId;
+	private MessageId replyId;
 
 	protected abstract ThreadListController<G, I, H> getController();
 
@@ -63,8 +64,6 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 		if (b == null) throw new IllegalStateException("No GroupId in intent.");
 		groupId = new GroupId(b);
 		getController().setGroupId(groupId);
-		String groupName = i.getStringExtra(GROUP_NAME);
-		setActionBarTitle(groupName);
 
 		textInput = (TextInputView) findViewById(R.id.text_input_container);
 		textInput.setVisibility(GONE);
@@ -76,27 +75,23 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 		list.setAdapter(adapter);
 
 		if (state != null) {
-			replyId = state.getByteArray(KEY_REPLY_ID);
+			replyId = new MessageId(state.getByteArray(KEY_REPLY_ID));
 		}
 
 		loadItems();
 	}
 
-	protected abstract @LayoutRes int getLayout();
+	@LayoutRes
+	protected abstract int getLayout();
 
 	protected abstract A createAdapter(LinearLayoutManager layoutManager);
 
-	protected void setActionBarTitle(@Nullable String title) {
-		if (title != null) setTitle(title);
-		else loadGroupItem();
-	}
-
-	protected void loadGroupItem() {
-		getController().loadGroupItem(
+	protected void loadNamedGroup() {
+		getController().loadNamedGroup(
 				new UiResultExceptionHandler<G, DbException>(this) {
 					@Override
 					public void onResultUi(G groupItem) {
-						onGroupItemLoaded(groupItem);
+						onNamedGroupLoaded(groupItem);
 					}
 
 					@Override
@@ -107,11 +102,8 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 				});
 	}
 
-	@CallSuper
 	@UiThread
-	protected void onGroupItemLoaded(G groupItem) {
-		setTitle(groupItem.getName());
-	}
+	protected abstract void onNamedGroupLoaded(G groupItem);
 
 	private void loadItems() {
 		getController().loadItems(
@@ -208,7 +200,7 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 		showTextInput(item);
 	}
 
-	protected void displaySnackbarShort(int stringId) {
+	protected void displaySnackbarShort(@StringRes int stringId) {
 		Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT);
 		snackbar.getView().setBackgroundResource(R.color.briar_primary);
 		snackbar.show();
@@ -233,6 +225,10 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 	public void onSendClick(String text) {
 		if (text.trim().length() == 0)
 			return;
+		if (text.length() > getMaxBodyLength()) {
+			displaySnackbarShort(R.string.text_too_long);
+			return;
+		}
 		I replyItem = adapter.getReplyItem();
 		UiResultExceptionHandler<I, DbException> handler =
 				new UiResultExceptionHandler<I, DbException>(this) {
@@ -258,6 +254,8 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 		adapter.setReplyItem(null);
 	}
 
+	protected abstract int getMaxBodyLength();
+
 	@Override
 	public void onHeaderReceived(H header) {
 		getController().loadItem(header,
@@ -302,8 +300,10 @@ public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadIt
 		}
 	}
 
-	protected abstract @StringRes int getItemPostedString();
+	@StringRes
+	protected abstract int getItemPostedString();
 
-	protected abstract @StringRes int getItemReceivedString();
+	@StringRes
+	protected abstract int getItemReceivedString();
 
 }
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
index ec876f3602c5e72375612d3a0e0983cbab7e2969..b62b6fd27788a2e7009f8b3a1eb59e443abb80b6 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
@@ -6,21 +6,20 @@ import android.support.annotation.UiThread;
 import org.briarproject.android.DestroyableContext;
 import org.briarproject.android.controller.ActivityLifecycleController;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.Collection;
 
-public interface ThreadListController<G extends BaseGroup, I extends ThreadItem, H extends PostHeader>
+public interface ThreadListController<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
 		extends ActivityLifecycleController {
 
 	void setGroupId(GroupId groupId);
 
-	void loadGroupItem(ResultExceptionHandler<G, DbException> handler);
+	void loadNamedGroup(ResultExceptionHandler<G, DbException> handler);
 
 	void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
 
@@ -35,7 +34,7 @@ public interface ThreadListController<G extends BaseGroup, I extends ThreadItem,
 	void send(String body, @Nullable MessageId parentId,
 			ResultExceptionHandler<I, DbException> handler);
 
-	void deleteGroupItem(ResultExceptionHandler<Void, DbException> handler);
+	void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);
 
 	interface ThreadListListener<H> extends DestroyableContext {
 		@UiThread
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
index 3fbf2bccc6c94217842e60b790dc000cf0b1d236..c412d2eaacfd11c435a13b8623544baf257d017a 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
@@ -7,8 +7,8 @@ import android.support.annotation.Nullable;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.controller.DbControllerImpl;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.api.clients.BaseGroup;
 import org.briarproject.api.clients.BaseMessage;
+import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.db.DatabaseExecutor;
@@ -33,7 +33,7 @@ import java.util.logging.Logger;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
-public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends ThreadItem, H extends PostHeader, M extends BaseMessage>
+public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends BaseMessage>
 		extends DbControllerImpl
 		implements ThreadListController<G, I, H>, EventListener {
 
@@ -47,7 +47,7 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 	protected final Map<MessageId, String> bodyCache =
 			new ConcurrentHashMap<>();
 
-	protected volatile GroupId groupId;
+	private volatile GroupId groupId;
 
 	protected ThreadListListener<H> listener;
 
@@ -110,7 +110,7 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 	}
 
 	@Override
-	public void loadGroupItem(
+	public void loadNamedGroup(
 			final ResultExceptionHandler<G, DbException> handler) {
 		checkGroupId();
 		runOnDbThread(new Runnable() {
@@ -121,7 +121,8 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 					G groupItem = loadGroupItem();
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
-						LOG.info("Loading forum took " + duration + " ms");
+						LOG.info(
+								"Loading named group took " + duration + " ms");
 					handler.onResult(groupItem);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -132,11 +133,7 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
+	@DatabaseExecutor
 	protected abstract G loadGroupItem() throws DbException;
 
 	@Override
@@ -157,7 +154,8 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 
 					// Load bodies
 					now = System.currentTimeMillis();
-					loadBodies(headers);
+					Map<MessageId, String> bodies = loadBodies(headers);
+					bodyCache.putAll(bodies);
 					duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading bodies took " + duration + " ms");
@@ -173,19 +171,11 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
+	@DatabaseExecutor
 	protected abstract Collection<H> loadHeaders() throws DbException;
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
-	protected abstract void loadBodies(Collection<H> headers)
+	@DatabaseExecutor
+	protected abstract Map<MessageId, String> loadBodies(Collection<H> headers)
 			throws DbException;
 
 	@Override
@@ -196,8 +186,10 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 			public void run() {
 				LOG.info("Loading item...");
 				try {
-					loadBodies(Collections.singletonList(header));
-					I item = buildItem(header);
+					String body = loadBodies(Collections.singletonList(header))
+							.get(header.getId());
+					bodyCache.put(header.getId(), body);
+					I item = buildItem(header, body);
 					handler.onResult(item);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -234,11 +226,7 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
+	@DatabaseExecutor
 	protected abstract void markRead(MessageId id) throws DbException;
 
 	@Override
@@ -255,9 +243,8 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 			public void run() {
 				LOG.info("Creating message...");
 				try {
-					M msg = createLocalMessage(groupId, body, parentId);
-					bodyCache.put(msg.getMessage().getId(), body);
-					storePost(msg, handler);
+					M msg = createLocalMessage(body, parentId);
+					storePost(msg, body, handler);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -267,15 +254,11 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
-	protected abstract M createLocalMessage(GroupId g, String body,
+	@DatabaseExecutor
+	protected abstract M createLocalMessage(String body,
 			@Nullable MessageId parentId) throws DbException;
 
-	private void storePost(final M p,
+	private void storePost(final M msg, final String body,
 			final ResultExceptionHandler<I, DbException> resultHandler) {
 		runOnDbThread(new Runnable() {
 			@Override
@@ -283,11 +266,12 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 				try {
 					LOG.info("Store message...");
 					long now = System.currentTimeMillis();
-					H h = addLocalMessage(p);
+					H header = addLocalMessage(msg);
+					bodyCache.put(msg.getMessage().getId(), body);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Storing message took " + duration + " ms");
-					resultHandler.onResult(buildItem(h));
+					resultHandler.onResult(buildItem(header, body));
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -297,15 +281,11 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
+	@DatabaseExecutor
 	protected abstract H addLocalMessage(M message) throws DbException;
 
 	@Override
-	public void deleteGroupItem(
+	public void deleteNamedGroup(
 			final ResultExceptionHandler<Void, DbException> handler) {
 		runOnDbThread(new Runnable() {
 			@Override
@@ -328,17 +308,13 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 		});
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
+	@DatabaseExecutor
 	protected abstract void deleteGroupItem(G groupItem) throws DbException;
 
 	private List<I> buildItems(Collection<H> headers) {
 		List<I> entries = new ArrayList<>();
 		for (H h : headers) {
-			entries.add(buildItem(h));
+			entries.add(buildItem(h, bodyCache.get(h.getId())));
 		}
 		return entries;
 	}
@@ -346,7 +322,12 @@ public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends Th
 	/**
 	 * When building the item, the body can be assumed to be cached
 	 */
-	protected abstract I buildItem(H header);
+	protected abstract I buildItem(H header, String body);
+
+	protected GroupId getGroupId() {
+		checkGroupId();
+		return groupId;
+	}
 
 	private void checkGroupId() {
 		if (groupId == null) {
diff --git a/briar-api/src/org/briarproject/api/blogs/Blog.java b/briar-api/src/org/briarproject/api/blogs/Blog.java
index 5de781e7de8875d3ee305466b59432594fb7cd6f..14bfbd97bdb883ef97db58ef34a88b3132e6b7db 100644
--- a/briar-api/src/org/briarproject/api/blogs/Blog.java
+++ b/briar-api/src/org/briarproject/api/blogs/Blog.java
@@ -2,19 +2,22 @@ package org.briarproject.api.blogs;
 
 import org.briarproject.api.clients.BaseGroup;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.Shareable;
 import org.briarproject.api.sync.Group;
 import org.jetbrains.annotations.NotNull;
 
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
 public class Blog extends BaseGroup implements Shareable {
 
 	private final String description;
 	private final Author author;
 
-	public Blog(@NotNull Group group, @NotNull String name,
-			@NotNull String description, @NotNull Author author) {
-		super(group, name, null);
-
+	public Blog(Group group, String name, String description, Author author) {
+		super(group, name);
 		this.description = description;
 		this.author = author;
 	}
diff --git a/briar-api/src/org/briarproject/api/clients/BaseGroup.java b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
index fe8a282f5b7fb50770be89e170dd3bf889d6171d..d2e1e8428a4a6442fd91f35d323f009f29021968 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseGroup.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
@@ -1,19 +1,22 @@
 package org.briarproject.api.clients;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.jetbrains.annotations.NotNull;
 
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
 public abstract class BaseGroup {
 
 	private final Group group;
 	private final String name;
-	private final byte[] salt;
 
-	public BaseGroup(@NotNull Group group, @NotNull String name, byte[] salt) {
+	public BaseGroup(Group group, String name) {
 		this.group = group;
 		this.name = name;
-		this.salt = salt;
 	}
 
 	@NotNull
@@ -31,10 +34,6 @@ public abstract class BaseGroup {
 		return name;
 	}
 
-	public byte[] getSalt() {
-		return salt;
-	}
-
 	@Override
 	public int hashCode() {
 		return group.hashCode();
diff --git a/briar-api/src/org/briarproject/api/clients/NamedGroup.java b/briar-api/src/org/briarproject/api/clients/NamedGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..c41429e7aca8f33963a68a0ba4db63c39d49225d
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/clients/NamedGroup.java
@@ -0,0 +1,29 @@
+package org.briarproject.api.clients;
+
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.Group;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+public abstract class NamedGroup extends BaseGroup {
+
+	private final byte[] salt;
+
+	public NamedGroup(@NotNull Group group, @NotNull String name, byte[] salt) {
+		super(group, name);
+		this.salt = salt;
+	}
+
+	public byte[] getSalt() {
+		return salt;
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return o instanceof NamedGroup && super.equals(o);
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java
index cdaa091b98960d65cce7d150e0994a785ab8e28a..3cbf297a07792509062813248ff34cb4bd9a8873 100644
--- a/briar-api/src/org/briarproject/api/forum/Forum.java
+++ b/briar-api/src/org/briarproject/api/forum/Forum.java
@@ -1,10 +1,15 @@
 package org.briarproject.api.forum;
 
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.Shareable;
 import org.briarproject.api.sync.Group;
 
-public class Forum extends BaseGroup implements Shareable {
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+public class Forum extends NamedGroup implements Shareable {
 
 	public Forum(Group group, String name, byte[] salt) {
 		super(group, name, salt);
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
index f6d5862c78da7a15f9f81adaf41d62c501d6b551..3141ab40b7af64a8a8bc73622766576ff4a06e95 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
@@ -1,11 +1,16 @@
 package org.briarproject.api.privategroup;
 
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.jetbrains.annotations.NotNull;
 
-public class PrivateGroup extends BaseGroup {
+import javax.annotation.concurrent.ThreadSafe;
+
+@ThreadSafe
+@NotNullByDefault
+public class PrivateGroup extends NamedGroup {
 
 	private final Author author;
 
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index ce47a5c3ee80adc3d6602ff0ec22d459f67a44a0..4983385c0a956514da6ebd08b7a13de43affd75e 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -154,7 +154,7 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 		} catch (GeneralSecurityException e) {
 			throw new RuntimeException(e);
 		} catch (FormatException e) {
-			throw new DbException(e);
+			throw new RuntimeException(e);
 		}
 		return p;
 	}