From 951457c99e89937ac85cd4dd71f593377fb917ee Mon Sep 17 00:00:00 2001
From: Daniel Gerhardt <code@dgerhardt.net>
Date: Thu, 10 Aug 2017 18:27:02 +0200
Subject: [PATCH] Move domain logic and caching to service layer

---
 .../de/thm/arsnova/cache/CacheBusterImpl.java |   2 +-
 .../arsnova/controller/MotdController.java    |  16 +-
 .../arsnova/controller/SessionController.java |   3 +-
 .../arsnova/persistance/AnswerRepository.java |   2 -
 .../persistance/CommentRepository.java        |   3 -
 .../persistance/ContentRepository.java        |   8 -
 .../arsnova/persistance/MotdRepository.java   |   2 -
 .../persistance/SessionRepository.java        |  16 +-
 .../couchdb/CouchDbAnswerRepository.java      |  27 --
 .../couchdb/CouchDbCommentRepository.java     |  42 ----
 .../couchdb/CouchDbContentRepository.java     | 156 ------------
 .../couchdb/CouchDbMotdListRepository.java    |   4 -
 .../couchdb/CouchDbMotdRepository.java        |  37 ---
 .../couchdb/CouchDbSessionRepository.java     | 118 +--------
 .../CouchDbSessionStatisticsRepository.java   |   2 -
 .../couchdb/CouchDbStatisticsRepository.java  |   2 -
 .../arsnova/services/CommentServiceImpl.java  |  12 +-
 .../thm/arsnova/services/ContentService.java  |   4 +-
 .../arsnova/services/ContentServiceImpl.java  | 231 +++++++++++-------
 .../de/thm/arsnova/services/MotdService.java  |   4 +-
 .../thm/arsnova/services/MotdServiceImpl.java |  30 ++-
 .../thm/arsnova/services/SessionService.java  |   2 +-
 .../arsnova/services/SessionServiceImpl.java  | 116 +++++++--
 .../services/StatisticsServiceImpl.java       |   8 +-
 .../score/VariantScoreCalculator.java         |  12 +-
 src/site/markdown/development/caching.md      |   2 +-
 26 files changed, 319 insertions(+), 542 deletions(-)

diff --git a/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java b/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java
index dad103c84..ac29ca5f8 100644
--- a/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java
+++ b/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java
@@ -51,7 +51,7 @@ public class CacheBusterImpl implements CacheBuster, ArsnovaEventVisitor {
 	@Override
 	public void visit(LockQuestionsEvent lockQuestionsEvent) { }
 
-	@CacheEvict(value = "answers", key = "#event.content")
+	@CacheEvict(value = "answers", key = "#event.content.id")
 	@Override
 	public void visit(NewAnswerEvent event) { }
 
diff --git a/src/main/java/de/thm/arsnova/controller/MotdController.java b/src/main/java/de/thm/arsnova/controller/MotdController.java
index acc933533..e9a19f58e 100644
--- a/src/main/java/de/thm/arsnova/controller/MotdController.java
+++ b/src/main/java/de/thm/arsnova/controller/MotdController.java
@@ -63,18 +63,18 @@ public class MotdController extends AbstractController {
 		@ApiParam(value = "sessionkey", required = false) @RequestParam(value = "sessionkey", defaultValue = "null") final String sessionkey
 	) {
 		List<Motd> motds;
-		Date client = new Date(System.currentTimeMillis());
+		Date date = new Date(System.currentTimeMillis());
 		if (!clientdate.isEmpty()) {
-			client.setTime(Long.parseLong(clientdate));
+			date.setTime(Long.parseLong(clientdate));
 		}
 		if (adminview) {
-			if ("null".equals(sessionkey)) {
-				motds = motdService.getAdminMotds();
-			} else {
-				motds = motdService.getAllSessionMotds(sessionkey);
-			}
+			motds = "session".equals(audience) ?
+					motdService.getAllSessionMotds(sessionkey) :
+					motdService.getAdminMotds();
 		} else {
-			motds = motdService.getCurrentMotds(client, audience, sessionkey);
+			motds = "session".equals(audience) ?
+					motdService.getCurrentSessionMotds(date, sessionkey) :
+					motdService.getCurrentMotds(date, audience);
 		}
 		return motds;
 	}
diff --git a/src/main/java/de/thm/arsnova/controller/SessionController.java b/src/main/java/de/thm/arsnova/controller/SessionController.java
index 3f11525fa..325818ccc 100644
--- a/src/main/java/de/thm/arsnova/controller/SessionController.java
+++ b/src/main/java/de/thm/arsnova/controller/SessionController.java
@@ -86,7 +86,8 @@ public class SessionController extends PaginationController {
 			nickname = "deleteSession")
 	@RequestMapping(value = "/{sessionkey}", method = RequestMethod.DELETE)
 	public void deleteSession(@ApiParam(value = "Session-Key from current session", required = true) @PathVariable final String sessionkey) {
-		sessionService.delete(sessionkey);
+		Session session = sessionService.getByKey(sessionkey);
+		sessionService.deleteCascading(session);
 	}
 
 	@ApiOperation(value = "count active users",
diff --git a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java
index 3f25db49d..dc2628a16 100644
--- a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java
@@ -33,8 +33,6 @@ public interface AnswerRepository extends CrudRepository<Answer, String> {
 	List<Answer> findByUserSessionId(User user, String sessionId);
 	int countBySessionKey(String sessionKey);
 	int deleteByContentId(String contentId);
-	void update(Answer answer);
-	void delete(String answerId);
 	int countBySessionIdLectureVariant(String sessionId);
 	int countBySessionIdPreparationVariant(String sessionId);
 	int deleteAllAnswersForQuestions(List<String> contentIds);
diff --git a/src/main/java/de/thm/arsnova/persistance/CommentRepository.java b/src/main/java/de/thm/arsnova/persistance/CommentRepository.java
index df5ddc8a5..6e97fb98a 100644
--- a/src/main/java/de/thm/arsnova/persistance/CommentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/CommentRepository.java
@@ -14,9 +14,6 @@ public interface CommentRepository extends CrudRepository<Comment, String> {
 	List<Comment> findBySessionId(String sessionId, int start, int limit);
 	List<Comment> findBySessionIdAndUser(String sessionId, User user, int start, int limit);
 	Comment findOne(String commentId);
-	Comment save(String sessionId, Comment comment, User user);
-	void markInterposedQuestionAsRead(Comment comment);
-	void delete(Comment comment);
 	int deleteBySessionId(String sessionId);
 	int deleteBySessionIdAndUser(String sessionId, User user);
 }
diff --git a/src/main/java/de/thm/arsnova/persistance/ContentRepository.java b/src/main/java/de/thm/arsnova/persistance/ContentRepository.java
index 7bea1b523..9dbef6ce1 100644
--- a/src/main/java/de/thm/arsnova/persistance/ContentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/ContentRepository.java
@@ -8,8 +8,6 @@ import java.util.List;
 
 public interface ContentRepository extends CrudRepository<Content, String> {
 	List<Content> findBySessionIdAndVariantAndActive(Object... keys);
-	Content findOne(String id);
-	Content save(String sessionId, Content content);
 	List<Content> findBySessionIdForUsers(String sessionId);
 	List<Content> findBySessionIdForSpeaker(String sessionId);
 	int countBySessionId(String sessionId);
@@ -17,7 +15,6 @@ public interface ContentRepository extends CrudRepository<Content, String> {
 	List<String> findIdsBySessionIdAndVariant(String sessionId, String variant);
 	int deleteBySessionId(String sessionId);
 	List<String> findUnansweredIdsBySessionIdAndUser(String sessionId, User user);
-	void update(Content content);
 	List<Content> findBySessionIdOnlyLectureVariantAndActive(String sessionId);
 	List<Content> findBySessionIdOnlyLectureVariant(String sessionId);
 	List<Content> findBySessionIdOnlyFlashcardVariantAndActive(String sessionId);
@@ -28,12 +25,7 @@ public interface ContentRepository extends CrudRepository<Content, String> {
 	int countLectureVariantBySessionId(String sessionId);
 	int countFlashcardVariantBySessionId(String sessionId);
 	int countPreparationVariantBySessionId(String sessionId);
-	void publishQuestions(String sessionId, boolean publish, List<Content> contents);
-	List<Content> publishAllQuestions(String sessionId, boolean publish);
 	List<String> findIdsBySessionIdAndVariantAndSubject(String sessionId, String questionVariant, String subject);
-	void resetQuestionsRoundState(String sessionId, List<Content> contents);
-	void setVotingAdmissions(String sessionId, boolean disableVoting, List<Content> contents);
-	List<Content> setVotingAdmissionForAllQuestions(String sessionId, boolean disableVoting);
 	List<String> findSubjectsBySessionIdAndVariant(String sessionId, String questionVariant);
 	List<String> findUnansweredIdsBySessionIdAndUserOnlyLectureVariant(String sessionId, User user);
 	List<String> findUnansweredIdsBySessionIdAndUserOnlyPreparationVariant(String sessionId, User user);
diff --git a/src/main/java/de/thm/arsnova/persistance/MotdRepository.java b/src/main/java/de/thm/arsnova/persistance/MotdRepository.java
index 44cdea7a3..deb9e9334 100644
--- a/src/main/java/de/thm/arsnova/persistance/MotdRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/MotdRepository.java
@@ -30,6 +30,4 @@ public interface MotdRepository extends CrudRepository<Motd, String> {
 	List<Motd> findForStudents();
 	List<Motd> findBySessionKey(String sessionkey);
 	Motd findByKey(String key);
-	Motd save(Motd motd);
-	void delete(Motd motd);
 }
diff --git a/src/main/java/de/thm/arsnova/persistance/SessionRepository.java b/src/main/java/de/thm/arsnova/persistance/SessionRepository.java
index 55597d5c9..43d88c292 100644
--- a/src/main/java/de/thm/arsnova/persistance/SessionRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/SessionRepository.java
@@ -28,26 +28,12 @@ import org.springframework.data.repository.CrudRepository;
 import java.util.List;
 
 public interface SessionRepository extends CrudRepository<Session, String> {
-	Session findOne(String sessionId);
 	Session findByKeyword(String keyword);
-	Session save(User user, Session session);
-	void update(Session session);
-
-	/**
-	 * Deletes a session and related data.
-	 *
-	 * @param session the session for deletion
-	 */
-	int[] deleteSession(Session session);
-
-	Session changeSessionCreator(Session session, String newCreator);
-	int[] deleteInactiveGuestSessions(long lastActivityBefore);
+	List<Session> findInactiveGuestSessionsMetadata(long lastActivityBefore);
 	List<Session> findByUser(User user, int start, int limit);
 	List<Session> findByUsername(String username, int start, int limit);
 	List<Session> findAllForPublicPool();
 	List<Session> findForPublicPoolByUser(User user);
-	boolean sessionKeyAvailable(String keyword);
-	Session updateSessionOwnerActivity(Session session);
 	List<Session> findVisitedByUsername(String username, int start, int limit);
 	List<SessionInfo> getMySessionsInfo(User user, int start, int limit);
 	List<SessionInfo> findInfosForPublicPool();
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java
index 3ecdfc18f..96f8e43ed 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java
@@ -16,7 +16,6 @@ import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.ApplicationEventPublisherAware;
 
@@ -41,7 +40,6 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple
 		this.publisher = publisher;
 	}
 
-	@CacheEvict("answers")
 	@Override
 	public int deleteByContentId(final String contentId) {
 		try {
@@ -178,31 +176,6 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple
 		return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt();
 	}
 
-	/* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */
-	@CacheEvict(value = "answers", allEntries = true)
-	public void update(final Answer answer) {
-		try {
-			super.update(answer);
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not update answer {}.", answer, e);
-			throw e;
-		}
-	}
-
-	/* TODO: Only evict cache entry for the answer's session. This requires some refactoring. */
-	@CacheEvict(value = "answers", allEntries = true)
-	@Override
-	public void delete(final String answerId) {
-		try {
-			/* TODO: use id and rev instead of loading the answer */
-			db.delete(get(answerId));
-			dbLogger.log("delete", "type", "answer");
-		} catch (final DbAccessException e) {
-			logger.error("Could not delete answer {}.", answerId, e);
-			throw e;
-		}
-	}
-
 	@Override
 	public int countBySessionIdLectureVariant(final String sessionId) {
 		return countBySessionIdVariant(sessionId, "lecture");
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java
index a71faeea7..6276c44fb 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java
@@ -142,48 +142,6 @@ public class CouchDbCommentRepository extends CouchDbCrudRepository<Comment> imp
 		return comments;
 	}
 
-	/* TODO: Move to service layer. */
-	@Override
-	public Comment save(final String sessionId, final Comment comment, final User user) {
-		/* TODO: This should be done on the service level. */
-		comment.setSessionId(sessionId);
-		comment.setCreator(user.getUsername());
-		comment.setRead(false);
-		if (comment.getTimestamp() == 0) {
-			comment.setTimestamp(System.currentTimeMillis());
-		}
-		try {
-			db.create(comment);
-
-			return comment;
-		} catch (final IllegalArgumentException e) {
-			logger.error("Could not save comment {}.", comment, e);
-		}
-
-		return null;
-	}
-
-	/* TODO: Move to service layer. */
-	@Override
-	public void markInterposedQuestionAsRead(final Comment comment) {
-		try {
-			comment.setRead(true);
-			db.update(comment);
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not mark comment as read {}.", comment.getId(), e);
-		}
-	}
-
-	@Override
-	public void delete(final Comment comment) {
-		try {
-			db.delete(comment.getId(), comment.getRevision());
-			dbLogger.log("delete", "type", "comment");
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not delete comment {}.", comment.getId(), e);
-		}
-	}
-
 	@Override
 	public int deleteBySessionId(final String sessionId) {
 		final ViewResult result = db.queryView(createQuery("by_sessionid").key(sessionId));
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java
index c09f433c7..ab9934fd1 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java
@@ -7,18 +7,11 @@ import de.thm.arsnova.persistance.LogEntryRepository;
 import org.ektorp.BulkDeleteDocument;
 import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
-import org.ektorp.DbAccessException;
-import org.ektorp.DocumentNotFoundException;
 import org.ektorp.DocumentOperationResult;
-import org.ektorp.UpdateConflictException;
 import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.CachePut;
-import org.springframework.cache.annotation.Cacheable;
-import org.springframework.cache.annotation.Caching;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -38,7 +31,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		super(Content.class, db, "by_sessionid", createIfNotExists);
 	}
 
-	@Cacheable("skillquestions")
 	@Override
 	public List<Content> findBySessionIdForUsers(final String sessionId) {
 		final List<Content> contents = new ArrayList<>();
@@ -52,7 +44,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return contents;
 	}
 
-	@Cacheable("skillquestions")
 	@Override
 	public List<Content> findBySessionIdForSpeaker(final String sessionId) {
 		return findBySessionIdAndVariantAndActive(new Object[] {sessionId}, sessionId);
@@ -67,66 +58,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return result.getSize();
 	}
 
-	/* TODO: Move to service layer. */
-	@Caching(evict = {@CacheEvict(value = "skillquestions", key = "#sessionId"),
-			@CacheEvict(value = "lecturequestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('lecture')"),
-			@CacheEvict(value = "preparationquestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('preparation')"),
-			@CacheEvict(value = "flashcardquestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('flashcard')") },
-			put = {@CachePut(value = "questions", key = "#content.id")})
-	@Override
-	public Content save(final String sessionId, final Content content) {
-		/* TODO: This should be done on the service level. */
-		content.setSessionId(sessionId);
-		try {
-			db.create(content);
-
-			return content;
-		} catch (final IllegalArgumentException e) {
-			logger.error("Could not save content {}.", content, e);
-		}
-
-		return null;
-	}
-
-	/* TODO: Move to service layer. */
-	/* TODO: Only evict cache entry for the content's session. This requires some refactoring. */
-	@Caching(evict = {@CacheEvict(value = "skillquestions", allEntries = true),
-			@CacheEvict(value = "lecturequestions", allEntries = true, condition = "#content.getQuestionVariant().equals('lecture')"),
-			@CacheEvict(value = "preparationquestions", allEntries = true, condition = "#content.getQuestionVariant().equals('preparation')"),
-			@CacheEvict(value = "flashcardquestions", allEntries = true, condition = "#content.getQuestionVariant().equals('flashcard')") },
-			put = {@CachePut(value = "questions", key = "#content.id")})
-	@Override
-	public void update(final Content content) {
-		try {
-			/* TODO: This should be done on the service level. Make sure that
-			 * sessionId is valid before so the content does not need to be retrieved. */
-			final Content oldContent = get(content.getId());
-			content.setId(oldContent.getId());
-			content.setRevision(oldContent.getRevision());
-			content.updateRoundManagementState();
-			super.update(content);
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not update content {}.", content, e);
-		}
-	}
-
-	/* TODO: Move to service layer. */
-	@Cacheable("questions")
-	@Override
-	public Content findOne(final String id) {
-		try {
-			final Content content = get(id);
-			content.updateRoundManagementState();
-			//content.setSessionKeyword(sessionRepository.getSessionFromId(content.getSessionId()).getKeyword());
-
-			return content;
-		} catch (final DocumentNotFoundException e) {
-			logger.error("Could not get question {}.", id, e);
-		}
-
-		return null;
-	}
-
 	@Override
 	public List<String> findIdsBySessionId(final String sessionId) {
 		return collectQuestionIds(db.queryView(createQuery("by_sessionid_variant_active")
@@ -197,7 +128,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return collectUnansweredQuestionIdsByPiRound(findBySessionIdOnlyPreparationVariantAndActive(sessionId), answeredQuestions);
 	}
 
-	@Cacheable("lecturequestions")
 	@Override
 	public List<Content> findBySessionIdOnlyLectureVariantAndActive(final String sessionId) {
 		return findBySessionIdAndVariantAndActive(sessionId, "lecture", true);
@@ -208,7 +138,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return findBySessionIdAndVariantAndActive(sessionId, "lecture");
 	}
 
-	@Cacheable("flashcardquestions")
 	@Override
 	public List<Content> findBySessionIdOnlyFlashcardVariantAndActive(final String sessionId) {
 		return findBySessionIdAndVariantAndActive(sessionId, "flashcard", true);
@@ -219,7 +148,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return findBySessionIdAndVariantAndActive(sessionId, "flashcard");
 	}
 
-	@Cacheable("preparationquestions")
 	@Override
 	public List<Content> findBySessionIdOnlyPreparationVariantAndActive(final String sessionId) {
 		return findBySessionIdAndVariantAndActive(sessionId, "preparation", true);
@@ -320,71 +248,6 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 		return ids;
 	}
 
-	/* TODO: Move to service layer. */
-	@Override
-	public List<Content> publishAllQuestions(final String sessionId, final boolean publish) {
-		final List<Content> contents = db.queryView(createQuery("by_sessionid_variant_active")
-						.startKey(ComplexKey.of(sessionId))
-						.endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject())),
-				Content.class);
-		/* FIXME: caching */
-		publishQuestions(sessionId, publish, contents);
-
-		return contents;
-	}
-
-	/* TODO: Move to service layer. */
-	@Caching(evict = { @CacheEvict(value = "contents", allEntries = true),
-			@CacheEvict(value = "skillquestions", key = "#sessionId"),
-			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
-			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
-			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
-	@Override
-	public void publishQuestions(final String sessionId, final boolean publish, final List<Content> contents) {
-		for (final Content content : contents) {
-			content.setActive(publish);
-		}
-		try {
-			db.executeBulk(contents);
-		} catch (final DbAccessException e) {
-			logger.error("Could not bulk publish all contents.", e);
-		}
-	}
-
-	/* TODO: Move to service layer. */
-	@Override
-	public List<Content> setVotingAdmissionForAllQuestions(final String sessionId, final boolean disableVoting) {
-		final List<Content> contents = db.queryView(createQuery("by_sessionid_variant_active")
-						.startKey(ComplexKey.of(sessionId))
-						.endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject()))
-						.includeDocs(true),
-				Content.class);
-		/* FIXME: caching */
-		setVotingAdmissions(sessionId, disableVoting, contents);
-
-		return contents;
-	}
-
-	@Caching(evict = { @CacheEvict(value = "contents", allEntries = true),
-			@CacheEvict(value = "skillquestions", key = "#sessionId"),
-			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
-			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
-			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
-	@Override
-	public void setVotingAdmissions(final String sessionId, final boolean disableVoting, final List<Content> contents) {
-		for (final Content q : contents) {
-			if (!"flashcard".equals(q.getQuestionType())) {
-				q.setVotingDisabled(disableVoting);
-			}
-		}
-
-		try {
-			db.executeBulk(contents);
-		} catch (final DbAccessException e) {
-			logger.error("Could not bulk set voting admission for all contents.", e);
-		}
-	}
-
 	/* TODO: remove if this method is no longer used */
 	@Override
 	public List<String> findIdsBySessionIdAndVariantAndSubject(final String sessionId, final String questionVariant, final String subject) {
@@ -416,23 +279,4 @@ public class CouchDbContentRepository extends CouchDbCrudRepository<Content> imp
 
 		return new ArrayList<>(uniqueSubjects);
 	}
-
-	/* TODO: Move to service layer. */
-	@Caching(evict = { @CacheEvict(value = "contents", allEntries = true),
-			@CacheEvict(value = "skillquestions", key = "#sessionId"),
-			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
-			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
-			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
-	@Override
-	public void resetQuestionsRoundState(final String sessionId, final List<Content> contents) {
-		for (final Content q : contents) {
-			q.setSessionId(sessionId);
-			q.resetQuestionState();
-		}
-		try {
-			db.executeBulk(contents);
-		} catch (final DbAccessException e) {
-			logger.error("Could not bulk reset all contents round state.", e);
-		}
-	}
 }
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java
index 1b7e36990..499013a87 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java
@@ -7,8 +7,6 @@ import org.ektorp.DbAccessException;
 import org.ektorp.support.CouchDbRepositorySupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.cache.annotation.CachePut;
-import org.springframework.cache.annotation.Cacheable;
 
 import java.util.List;
 
@@ -20,7 +18,6 @@ public class CouchDbMotdListRepository extends CouchDbRepositorySupport<MotdList
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motdlist", key = "#p0")
 	public MotdList findByUsername(final String username) {
 		final List<MotdList> motdListList = queryView("by_username", username);
 		return motdListList.isEmpty() ? new MotdList() : motdListList.get(0);
@@ -28,7 +25,6 @@ public class CouchDbMotdListRepository extends CouchDbRepositorySupport<MotdList
 
 	/* TODO: Move to service layer. */
 	@Override
-	@CachePut(cacheNames = "motdlist", key = "#p0.username")
 	public MotdList save(final MotdList motdlist) {
 		try {
 			if (motdlist.getId() != null) {
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java
index 8c2de65f8..7a00e2886 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java
@@ -19,13 +19,9 @@ package de.thm.arsnova.persistance.couchdb;
 
 import de.thm.arsnova.entities.Motd;
 import de.thm.arsnova.persistance.MotdRepository;
-import de.thm.arsnova.services.SessionService;
 import org.ektorp.CouchDbConnector;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -33,9 +29,6 @@ import java.util.List;
 public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implements MotdRepository {
 	private static final Logger logger = LoggerFactory.getLogger(CouchDbMotdRepository.class);
 
-	@Autowired
-	private SessionService sessionService;
-
 	public CouchDbMotdRepository(final CouchDbConnector db, final boolean createIfNotExists) {
 		super(Motd.class, db, "by_sessionkey", createIfNotExists);
 	}
@@ -46,19 +39,16 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motds", key = "'all'")
 	public List<Motd> findGlobalForAll() {
 		return find("by_audience_for_global", "all");
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motds", key = "'loggedIn'")
 	public List<Motd> findGlobalForLoggedIn() {
 		return find("by_audience_for_global", "loggedIn");
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motds", key = "'tutors'")
 	public List<Motd> findGlobalForTutors() {
 		final List<Motd> union = new ArrayList<>();
 		union.addAll(find("by_audience_for_global", "loggedIn"));
@@ -68,7 +58,6 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motds", key = "'students'")
 	public List<Motd> findForStudents() {
 		final List<Motd> union = new ArrayList<>();
 		union.addAll(find("by_audience_for_global", "loggedIn"));
@@ -78,7 +67,6 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement
 	}
 
 	@Override
-	@Cacheable(cacheNames = "motds", key = "('session').concat(#p0)")
 	public List<Motd> findBySessionKey(final String sessionkey) {
 		return find("by_sessionkey", sessionkey);
 	}
@@ -93,29 +81,4 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement
 
 		return motd.get(0);
 	}
-
-	@Override
-	@CacheEvict(cacheNames = "motds", key = "#p0.audience.concat(#p0.sessionkey)")
-	public Motd save(final Motd motd) {
-		final String id = motd.getId();
-		final String rev = motd.getRevision();
-
-		if (null != id) {
-			Motd oldMotd = get(id);
-			motd.setMotdkey(oldMotd.getMotdkey());
-			update(motd);
-		} else {
-			motd.setMotdkey(sessionService.generateKey());
-			add(motd);
-		}
-
-		return motd;
-	}
-
-	/* TODO: Redundant -> remove. Move cache handling to service layer. */
-	@Override
-	@CacheEvict(cacheNames = "motds", key = "#p0.audience.concat(#p0.sessionkey)")
-	public void delete(final Motd motd) {
-		db.delete(motd);
-	}
 }
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java
index a21ecbab9..91be570e1 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java
@@ -29,7 +29,6 @@ import de.thm.arsnova.exceptions.NotFoundException;
 import de.thm.arsnova.persistance.LogEntryRepository;
 import de.thm.arsnova.persistance.MotdRepository;
 import de.thm.arsnova.persistance.SessionRepository;
-import de.thm.arsnova.services.SessionService;
 import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
 import org.ektorp.DocumentNotFoundException;
@@ -39,10 +38,7 @@ import org.ektorp.ViewResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.CachePut;
 import org.springframework.cache.annotation.Cacheable;
-import org.springframework.cache.annotation.Caching;
 
 import java.io.IOException;
 import java.util.AbstractMap;
@@ -57,9 +53,6 @@ import java.util.stream.Collectors;
 public class CouchDbSessionRepository extends CouchDbCrudRepository<Session> implements SessionRepository {
 	private static final Logger logger = LoggerFactory.getLogger(CouchDbSessionRepository.class);
 
-	@Autowired
-	private SessionService sessionService;
-
 	@Autowired
 	private LogEntryRepository dbLogger;
 
@@ -78,37 +71,6 @@ public class CouchDbSessionRepository extends CouchDbCrudRepository<Session> imp
 		return !session.isEmpty() ? session.get(0) : null;
 	}
 
-	/* TODO: Redundant -> remove. Move cache handling to service layer. */
-	@Override
-	@Cacheable("sessions")
-	public Session findOne(final String sessionId) {
-		return get(sessionId);
-	}
-
-	/* TODO: Move to service layer. */
-	@Override
-	@Caching(evict = @CacheEvict(cacheNames = "sessions", key = "#result.keyword"))
-	public Session save(final User user, final Session session) {
-		session.setKeyword(sessionService.generateKey());
-		session.setCreator(user.getUsername());
-		session.setActive(true);
-		session.setFeedbackLock(false);
-
-		try {
-			db.create(session);
-		} catch (final IllegalArgumentException e) {
-			logger.error("Could not save session to database.", e);
-		}
-
-		return session.getId() != null ? session : null;
-	}
-
-	/* TODO: Move to service layer. */
-	@Override
-	public boolean sessionKeyAvailable(final String keyword) {
-		return findByKeyword(keyword) == null;
-	}
-
 	/* TODO: Move to service layer. */
 	private String getSessionKeyword(final String internalSessionId) throws IOException {
 		final Session session = get(internalSessionId);
@@ -121,26 +83,6 @@ public class CouchDbSessionRepository extends CouchDbCrudRepository<Session> imp
 		return session.getKeyword();
 	}
 
-	/* TODO: Move to service layer. */
-	@Override
-	@CachePut(value = "sessions")
-	public Session updateSessionOwnerActivity(final Session session) {
-		try {
-			/* Do not clutter CouchDB. Only update once every 3 hours. */
-			if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) {
-				return session;
-			}
-
-			session.setLastOwnerActivity(System.currentTimeMillis());
-			update(session);
-
-			return session;
-		} catch (final UpdateConflictException e) {
-			logger.error("Failed to update lastOwnerActivity for session {}.", session, e);
-			return session;
-		}
-	}
-
 	@Override
 	public List<Session> findVisitedByUsername(final String username, final int start, final int limit) {
 		final int qSkip = start > 0 ? start : -1;
@@ -216,73 +158,21 @@ public class CouchDbSessionRepository extends CouchDbCrudRepository<Session> imp
 				ComplexKey.of(courses.stream().map(Course::getId).collect(Collectors.toList())));
 	}
 
-	/* TODO: Redundant -> remove. Move cache handling to service layer. */
-	@Override
-	@CachePut(value = "sessions")
-	public void update(final Session session) {
-		try {
-			super.update(session);
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not update session {}.", session, e);
-		}
-	}
-
-	/* TODO: Move to service layer. */
-	@Override
-	@Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#p0.keyword") })
-	public Session changeSessionCreator(final Session session, final String newCreator) {
-		final Session s = get(session.getId());
-		s.setCreator(newCreator);
-		try {
-			update(s);
-		} catch (final UpdateConflictException e) {
-			logger.error("Could not update creator for session {}.", session, e);
-		}
-
-		return s;
-	}
-
 	@Override
-	@Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#p0.keyword") })
-	public int[] deleteSession(final Session session) {
-		/* FIXME: not yet migrated - move to service layer */
-		throw new UnsupportedOperationException();
-//		final int[] count = new int[] {0, 0};
-//		try {
-//			count = deleteBySessionId(session);
-//			remove(session);
-//			logger.debug("Deleted session document {} and related data.", session.getId());
-//			dbLogger.log("delete", "type", "session", "id", session.getId());
-//		} catch (final Exception e) {
-//			/* TODO: improve error handling */
-//			logger.error("Could not delete session {}.", session, e);
-//		}
-//
-//		return count;
-	}
-
-	@Override
-	public int[] deleteInactiveGuestSessions(final long lastActivityBefore) {
+	public List<Session> findInactiveGuestSessionsMetadata(final long lastActivityBefore) {
 		final ViewResult result = db.queryView(
 				createQuery("by_lastactivity_for_guests").endKey(lastActivityBefore));
 		final int[] count = new int[3];
 
+		List<Session> sessions = new ArrayList<>();
 		for (final ViewResult.Row row : result.getRows()) {
 			final Session s = new Session();
 			s.setId(row.getId());
 			s.setRevision(row.getValueAsNode().get("_rev").asText());
-			final int[] qaCount = deleteSession(s);
-			count[1] += qaCount[0];
-			count[2] += qaCount[1];
-		}
-
-		if (!result.isEmpty()) {
-			logger.info("Deleted {} inactive guest sessions.", result.getSize());
-			dbLogger.log("cleanup", "type", "session", "sessionCount", result.getSize(), "questionCount", count[1], "answerCount", count[2]);
+			sessions.add(s);
 		}
-		count[0] = result.getSize();
 
-		return count;
+		return sessions;
 	}
 
 	/* TODO: Move to service layer. */
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java
index 8796f056e..e0c5f4927 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java
@@ -8,14 +8,12 @@ import org.ektorp.ComplexKey;
 import org.ektorp.CouchDbConnector;
 import org.ektorp.ViewResult;
 import org.ektorp.support.CouchDbRepositorySupport;
-import org.springframework.cache.annotation.Cacheable;
 
 public class CouchDbSessionStatisticsRepository extends CouchDbRepositorySupport implements SessionStatisticsRepository {
 	public CouchDbSessionStatisticsRepository(final CouchDbConnector db, final boolean createIfNotExists) {
 		super(Object.class, db, "learning_progress", createIfNotExists);
 	}
 
-	@Cacheable("learningprogress")
 	@Override
 	public Score getLearningProgress(final Session session) {
 		final ViewResult maximumValueResult = db.queryView(createQuery("maximum_value_of_question")
diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java
index f3b27feff..3506f7da4 100644
--- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java
+++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java
@@ -8,7 +8,6 @@ import org.ektorp.ViewResult;
 import org.ektorp.support.CouchDbRepositorySupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.cache.annotation.Cacheable;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -20,7 +19,6 @@ public class CouchDbStatisticsRepository extends CouchDbRepositorySupport implem
 		super(Object.class, db, "statistics", createIfNotExists);
 	}
 
-	@Cacheable("statistics")
 	@Override
 	public Statistics getStatistics() {
 		final Statistics stats = new Statistics();
diff --git a/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java b/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java
index 01d71afe6..1a1428994 100644
--- a/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java
@@ -53,7 +53,14 @@ public class CommentServiceImpl extends EntityService<Comment> implements Commen
 	@PreAuthorize("isAuthenticated()")
 	public boolean save(final Comment comment) {
 		final Session session = sessionRepository.findByKeyword(comment.getSessionId());
-		final Comment result = commentRepository.save(session.getId(), comment, userService.getCurrentUser());
+		final User user = userService.getCurrentUser();
+		comment.setSessionId(session.getId());
+		comment.setCreator(user.getUsername());
+		comment.setRead(false);
+		if (comment.getTimestamp() == 0) {
+			comment.setTimestamp(System.currentTimeMillis());
+		}
+		final Comment result = super.create(comment);
 
 		if (null != result) {
 			final NewCommentEvent event = new NewCommentEvent(this, session, result);
@@ -151,7 +158,8 @@ public class CommentServiceImpl extends EntityService<Comment> implements Commen
 			throw new UnauthorizedException();
 		}
 		if (session.isCreator(user)) {
-			commentRepository.markInterposedQuestionAsRead(comment);
+			comment.setRead(true);
+			save(comment);
 		}
 		return comment;
 	}
diff --git a/src/main/java/de/thm/arsnova/services/ContentService.java b/src/main/java/de/thm/arsnova/services/ContentService.java
index c2735488f..0c3f13037 100644
--- a/src/main/java/de/thm/arsnova/services/ContentService.java
+++ b/src/main/java/de/thm/arsnova/services/ContentService.java
@@ -72,9 +72,9 @@ public interface ContentService {
 
 	int countTotalAnswersByQuestionId(String questionId);
 
-	Content update(Content content);
+	Content save(final String sessionId, final Content content);
 
-	Content update(Content content, User user);
+	Content update(Content content);
 
 	void deleteAnswers(String questionId);
 
diff --git a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java
index 1318d12f9..d7f9ef655 100644
--- a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java
@@ -32,11 +32,14 @@ import de.thm.arsnova.persistance.AnswerRepository;
 import de.thm.arsnova.persistance.ContentRepository;
 import de.thm.arsnova.persistance.SessionRepository;
 import org.ektorp.DbAccessException;
+import org.ektorp.DocumentNotFoundException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.cache.annotation.Caching;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.ApplicationEventPublisherAware;
@@ -128,8 +131,90 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		}
 	}
 
+	@Cacheable("questions")
+	@Override
+	public Content get(final String id) {
+		try {
+			final Content content = super.get(id);
+			if (!"freetext".equals(content.getQuestionType()) && 0 == content.getPiRound()) {
+			/* needed for legacy questions whose piRound property has not been set */
+				content.setPiRound(1);
+			}
+			content.updateRoundManagementState();
+			//content.setSessionKeyword(sessionRepository.getSessionFromId(content.getSessionId()).getKeyword());
+
+			return content;
+		} catch (final DocumentNotFoundException e) {
+			logger.error("Could not get question {}.", id, e);
+		}
+
+		return null;
+	}
+
+	@Override
+	@Caching(evict = {@CacheEvict(value = "skillquestions", key = "#sessionId"),
+			@CacheEvict(value = "lecturequestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('lecture')"),
+			@CacheEvict(value = "preparationquestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('preparation')"),
+			@CacheEvict(value = "flashcardquestions", key = "#sessionId", condition = "#content.getQuestionVariant().equals('flashcard')") },
+			put = {@CachePut(value = "questions", key = "#content.id")})
+	public Content save(final String sessionId, final Content content) {
+		content.setSessionId(sessionId);
+		try {
+			contentRepository.save(content);
+
+			return content;
+		} catch (final IllegalArgumentException e) {
+			logger.error("Could not save content {}.", content, e);
+		}
+
+		return null;
+	}
+
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@Caching(evict = {
+			@CacheEvict(value = "skillquestions", allEntries = true),
+			@CacheEvict(value = "lecturequestions", allEntries = true, condition = "#content.getQuestionVariant().equals('lecture')"),
+			@CacheEvict(value = "preparationquestions", allEntries = true, condition = "#content.getQuestionVariant().equals('preparation')"),
+			@CacheEvict(value = "flashcardquestions", allEntries = true, condition = "#content.getQuestionVariant().equals('flashcard')") },
+			put = {@CachePut(value = "questions", key = "#content.id")})
+	public Content update(final Content content) {
+		final User user = userService.getCurrentUser();
+		final Content oldContent = contentRepository.findOne(content.getId());
+		if (null == oldContent) {
+			throw new NotFoundException();
+		}
+
+		final Session session = sessionRepository.findOne(content.getSessionId());
+		if (user == null || session == null || !session.isCreator(user)) {
+			throw new UnauthorizedException();
+		}
+
+		if ("freetext".equals(content.getQuestionType())) {
+			content.setPiRound(0);
+		} else if (content.getPiRound() < 1 || content.getPiRound() > 2) {
+			content.setPiRound(oldContent.getPiRound() > 0 ? oldContent.getPiRound() : 1);
+		}
+
+		content.setId(oldContent.getId());
+		content.setRevision(oldContent.getRevision());
+		content.updateRoundManagementState();
+		contentRepository.save(content);
+
+		if (!oldContent.isActive() && content.isActive()) {
+			final UnlockQuestionEvent event = new UnlockQuestionEvent(this, session, content);
+			this.publisher.publishEvent(event);
+		} else if (oldContent.isActive() && !content.isActive()) {
+			final LockQuestionEvent event = new LockQuestionEvent(this, session, content);
+			this.publisher.publishEvent(event);
+		}
+		return content;
+	}
+
+	/* FIXME: caching */
+	@Override
+	@PreAuthorize("isAuthenticated()")
+	//@Cacheable("skillquestions")
 	public List<Content> getBySessionKey(final String sessionkey) {
 		final Session session = getSession(sessionkey);
 		final User user = userService.getCurrentUser();
@@ -171,7 +256,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 			}
 		}
 
-		final Content result = contentRepository.save(session.getId(), content);
+		final Content result = save(session.getId(), content);
 
 		final NewQuestionEvent event = new NewQuestionEvent(this, session, result);
 		this.publisher.publishEvent(event);
@@ -179,25 +264,11 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		return result;
 	}
 
-	@Override
-	@PreAuthorize("isAuthenticated()")
-	public Content get(final String id) {
-		final Content result = contentRepository.findOne(id);
-		if (result == null) {
-			return null;
-		}
-		if (!"freetext".equals(result.getQuestionType()) && 0 == result.getPiRound()) {
-			/* needed for legacy questions whose piRound property has not been set */
-			result.setPiRound(1);
-		}
-
-		return result;
-	}
-
 	/* TODO: Only evict cache entry for the content's session. This requires some refactoring. */
 	@Override
 	@PreAuthorize("isAuthenticated() and hasPermission(#contentId, 'content', 'owner')")
 	@Caching(evict = {
+			@CacheEvict("answers"),
 			@CacheEvict(value = "questions", key = "#contentId"),
 			@CacheEvict(value = "skillquestions", allEntries = true),
 			@CacheEvict(value = "lecturequestions", allEntries = true /*, condition = "#content.getQuestionVariant().equals('lecture')"*/),
@@ -293,7 +364,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		content.setPiRoundEndTime(0);
 		content.setVotingDisabled(true);
 		content.updateRoundManagementState();
-		update(content, user);
+		update(content);
 
 		this.publisher.publishEvent(new PiRoundEndEvent(this, session, content));
 	}
@@ -356,6 +427,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 
 	@Override
 	@PreAuthorize("isAuthenticated() and hasPermission(#questionId, 'content', 'owner')")
+	@CacheEvict("answers")
 	public void resetPiRoundState(final String questionId) {
 		final Content content = contentRepository.findOne(questionId);
 		final Session session = sessionRepository.findOne(content.getSessionId());
@@ -384,7 +456,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 			content.setActive(true);
 			update(content);
 		} else {
-			contentRepository.update(content);
+			update(content);
 		}
 		ArsnovaEvent event;
 		if (disableVoting) {
@@ -397,13 +469,22 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@Caching(evict = { @CacheEvict(value = "contents", allEntries = true),
+			@CacheEvict(value = "skillquestions", key = "#sessionId"),
+			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
+			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
+			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
 	public void setVotingAdmissions(final String sessionkey, final boolean disableVoting, List<Content> contents) {
 		final User user = getCurrentUser();
 		final Session session = getSession(sessionkey);
 		if (!session.isCreator(user)) {
 			throw new UnauthorizedException();
 		}
-		contentRepository.setVotingAdmissions(session.getId(), disableVoting, contents);
+		for (final Content q : contents) {
+			if (!"flashcard".equals(q.getQuestionType())) {
+				q.setVotingDisabled(disableVoting);
+			}
+		}
 		ArsnovaEvent event;
 		if (disableVoting) {
 			event = new LockVotesEvent(this, session, contents);
@@ -421,14 +502,8 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		if (!session.isCreator(user)) {
 			throw new UnauthorizedException();
 		}
-		final List<Content> contents = contentRepository.setVotingAdmissionForAllQuestions(session.getId(), disableVoting);
-		ArsnovaEvent event;
-		if (disableVoting) {
-			event = new LockVotesEvent(this, session, contents);
-		} else {
-			event = new UnlockVotesEvent(this, session, contents);
-		}
-		this.publisher.publishEvent(event);
+		final List<Content> contents = contentRepository.findBySessionId(session.getId());
+		setVotingAdmissionForAllQuestions(session.getId(), disableVoting);
 	}
 
 	private Session getSessionWithAuthCheck(final String sessionKeyword) {
@@ -445,7 +520,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 	public void deleteAnswers(final String questionId) {
 		final Content content = contentRepository.findOne(questionId);
 		content.resetQuestionState();
-		contentRepository.update(content);
+		update(content);
 		answerRepository.deleteByContentId(content.getId());
 	}
 
@@ -487,7 +562,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		final Session session = sessionRepository.findOne(answer.getSessionId());
 		if (session.isCreator(user)) {
 			answer.setRead(true);
-			answerRepository.update(answer);
+			answerRepository.save(answer);
 		}
 	}
 
@@ -599,7 +674,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 	public List<Answer> getMyAnswersBySessionKey(final String sessionKey) {
 		final Session session = getSession(sessionKey);
 		// Load contents first because we are only interested in answers of the latest piRound.
-		final List<Content> contents = contentRepository.findBySessionIdForUsers(session.getId());
+		final List<Content> contents = getBySessionKey(sessionKey);
 		final Map<String, Content> questionIdToQuestion = new HashMap<>();
 		for (final Content content : contents) {
 			questionIdToQuestion.put(content.getId(), content);
@@ -636,48 +711,10 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
-	public Content update(final Content content) {
-		final User user = userService.getCurrentUser();
-		return update(content, user);
-	}
-
-	@Override
-	@PreAuthorize("isAuthenticated()")
-	public Content update(final Content content, User user) {
-		final Content oldContent = contentRepository.findOne(content.getId());
-		if (null == oldContent) {
-			throw new NotFoundException();
-		}
-
-		final Session session = sessionRepository.findOne(content.getSessionId());
-		if (user == null || session == null || !session.isCreator(user)) {
-			throw new UnauthorizedException();
-		}
-
-		if ("freetext".equals(content.getQuestionType())) {
-			content.setPiRound(0);
-		} else if (content.getPiRound() < 1 || content.getPiRound() > 2) {
-			content.setPiRound(oldContent.getPiRound() > 0 ? oldContent.getPiRound() : 1);
-		}
-
-		contentRepository.update(content);
-
-		if (!oldContent.isActive() && content.isActive()) {
-			final UnlockQuestionEvent event = new UnlockQuestionEvent(this, session, content);
-			this.publisher.publishEvent(event);
-		} else if (oldContent.isActive() && !content.isActive()) {
-			final LockQuestionEvent event = new LockQuestionEvent(this, session, content);
-			this.publisher.publishEvent(event);
-		}
-		return content;
-	}
-
-	@Override
-	@PreAuthorize("isAuthenticated()")
-	@CacheEvict(value = "answers", key = "#content")
-	public Answer saveAnswer(final String questionId, final de.thm.arsnova.entities.transport.Answer answer) {
+	@CacheEvict(value = "answers", key = "#contentId")
+	public Answer saveAnswer(final String contentId, final de.thm.arsnova.entities.transport.Answer answer) {
 		final User user = getCurrentUser();
-		final Content content = get(questionId);
+		final Content content = get(contentId);
 		if (content == null) {
 			throw new NotFoundException();
 		}
@@ -707,6 +744,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@CacheEvict(value = "answers", allEntries = true)
 	public Answer updateAnswer(final Answer answer) {
 		final User user = userService.getCurrentUser();
 		final Answer realAnswer = this.getMyAnswer(answer.getQuestionId());
@@ -723,7 +761,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		answer.setUser(user.getUsername());
 		answer.setQuestionId(content.getId());
 		answer.setSessionId(session.getId());
-		answerRepository.update(realAnswer);
+		answerRepository.save(realAnswer);
 		this.publisher.publishEvent(new NewAnswerEvent(this, session, answer, user, content));
 
 		return answer;
@@ -731,6 +769,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@CacheEvict(value = "answers", allEntries = true)
 	public void deleteAnswer(final String questionId, final String answerId) {
 		final Content content = contentRepository.findOne(questionId);
 		if (content == null) {
@@ -746,8 +785,10 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		this.publisher.publishEvent(new DeleteAnswerEvent(this, session, content));
 	}
 
+	/* FIXME: caching */
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	//@Cacheable("lecturequestions")
 	public List<Content> getLectureQuestions(final String sessionkey) {
 		final Session session = getSession(sessionkey);
 		final User user = userService.getCurrentUser();
@@ -758,8 +799,10 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		}
 	}
 
+	/* FIXME: caching */
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	//@Cacheable("flashcardquestions")
 	public List<Content> getFlashcards(final String sessionkey) {
 		final Session session = getSession(sessionkey);
 		final User user = userService.getCurrentUser();
@@ -770,8 +813,10 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		}
 	}
 
+	/* FIXME: caching */
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	//@Cacheable("preparationquestions")
 	public List<Content> getPreparationQuestions(final String sessionkey) {
 		final Session session = getSession(sessionkey);
 		final User user = userService.getCurrentUser();
@@ -904,30 +949,33 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 	@Override
 	@PreAuthorize("isAuthenticated()")
 	public void publishAll(final String sessionkey, final boolean publish) {
+		/* TODO: resolve redundancies */
 		final User user = getCurrentUser();
 		final Session session = getSession(sessionkey);
 		if (!session.isCreator(user)) {
 			throw new UnauthorizedException();
 		}
-		final List<Content> contents = contentRepository.publishAllQuestions(session.getId(), publish);
-		ArsnovaEvent event;
-		if (publish) {
-			event = new UnlockQuestionsEvent(this, session, contents);
-		} else {
-			event = new LockQuestionsEvent(this, session, contents);
-		}
-		this.publisher.publishEvent(event);
+		final List<Content> contents = contentRepository.findBySessionId(session.getId());
+		publishQuestions(sessionkey, publish, contents);
 	}
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@Caching(evict = { @CacheEvict(value = "contents", allEntries = true),
+			@CacheEvict(value = "skillquestions", key = "#sessionId"),
+			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
+			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
+			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
 	public void publishQuestions(final String sessionkey, final boolean publish, List<Content> contents) {
 		final User user = getCurrentUser();
 		final Session session = getSession(sessionkey);
 		if (!session.isCreator(user)) {
 			throw new UnauthorizedException();
 		}
-		contentRepository.publishQuestions(session.getId(), publish, contents);
+		for (final Content content : contents) {
+			content.setActive(publish);
+		}
+		contentRepository.save(contents);
 		ArsnovaEvent event;
 		if (publish) {
 			event = new UnlockQuestionsEvent(this, session, contents);
@@ -948,7 +996,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		}
 
 		final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId());
-		contentRepository.resetQuestionsRoundState(session.getId(), contents);
+		resetContentsRoundState(session.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
 		answerRepository.deleteAllAnswersForQuestions(contentIds);
 
@@ -963,7 +1011,7 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		final Session session = getSession(sessionkey);
 
 		final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId(), "preparation");
-		contentRepository.resetQuestionsRoundState(session.getId(), contents);
+		resetContentsRoundState(session.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
 		answerRepository.deleteAllAnswersForQuestions(contentIds);
 
@@ -978,13 +1026,28 @@ public class ContentServiceImpl extends EntityService<Content> implements Conten
 		final Session session = getSession(sessionkey);
 
 		final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId(), "lecture");
-		contentRepository.resetQuestionsRoundState(session.getId(), contents);
+		resetContentsRoundState(session.getId(), contents);
 		final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList());
 		answerRepository.deleteAllAnswersForQuestions(contentIds);
 
 		this.publisher.publishEvent(new DeleteAllLectureAnswersEvent(this, session));
 	}
 
+	@Caching(evict = {
+			@CacheEvict(value = "contents", allEntries = true),
+			@CacheEvict(value = "skillquestions", key = "#sessionId"),
+			@CacheEvict(value = "lecturequestions", key = "#sessionId"),
+			@CacheEvict(value = "preparationquestions", key = "#sessionId"),
+			@CacheEvict(value = "flashcardquestions", key = "#sessionId") })
+	private void resetContentsRoundState(final String sessionId, final List<Content> contents) {
+		for (final Content q : contents) {
+			/* TODO: Check if setting the sessionId is necessary. */
+			q.setSessionId(sessionId);
+			q.resetQuestionState();
+		}
+		contentRepository.save(contents);
+	}
+
 	@Override
 	public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
 		this.publisher = publisher;
diff --git a/src/main/java/de/thm/arsnova/services/MotdService.java b/src/main/java/de/thm/arsnova/services/MotdService.java
index c010663a9..177e721d5 100644
--- a/src/main/java/de/thm/arsnova/services/MotdService.java
+++ b/src/main/java/de/thm/arsnova/services/MotdService.java
@@ -33,7 +33,9 @@ public interface MotdService {
 
 	List<Motd> getAllSessionMotds(final String sessionkey);
 
-	List<Motd> getCurrentMotds(final Date clientdate, final String audience, final String sessionkey);
+	List<Motd> getCurrentMotds(final Date clientdate, final String audience);
+
+	List<Motd> getCurrentSessionMotds(final Date clientdate, final String sessionkey);
 
 	List<Motd> filterMotdsByDate(List<Motd> list, Date clientdate);
 
diff --git a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java b/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java
index 1400daf56..86c0382fb 100644
--- a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java
@@ -25,6 +25,9 @@ import de.thm.arsnova.exceptions.BadRequestException;
 import de.thm.arsnova.persistance.MotdListRepository;
 import de.thm.arsnova.persistance.MotdRepository;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Service;
@@ -79,16 +82,24 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 	}
 
 	@Override
-	public List<Motd> getCurrentMotds(final Date clientdate, final String audience, final String sessionkey) {
+	@Cacheable(cacheNames = "motds", key = "('session').concat(#sessionkey)")
+	public List<Motd> getCurrentSessionMotds(final Date clientdate, final String sessionkey) {
+		final List<Motd> motds = motdRepository.findBySessionKey(sessionkey);
+		return filterMotdsByDate(motds, clientdate);
+	}
+
+	@Override
+	@Cacheable(cacheNames = "motds", key = "#audience")
+	public List<Motd> getCurrentMotds(final Date clientdate, final String audience) {
 		final List<Motd> motds;
 		switch (audience) {
 			case "all": motds = motdRepository.findGlobalForAll(); break;
 			case "loggedIn": motds = motdRepository.findGlobalForLoggedIn(); break;
 			case "students": motds = motdRepository.findForStudents(); break;
 			case "tutors": motds = motdRepository.findGlobalForTutors(); break;
-			case "session": motds = motdRepository.findBySessionKey(sessionkey); break;
-			default: motds = motdRepository.findGlobalForAll(); break;
+			default: throw new IllegalArgumentException("Invalid audience.");
 		}
+
 		return filterMotdsByDate(motds, clientdate);
 	}
 
@@ -135,7 +146,6 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 		Session session = sessionService.getByKey(sessionkey);
 		motd.setSessionId(session.getId());
 
-
 		return createOrUpdateMotd(motd);
 	}
 
@@ -151,6 +161,7 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 		return createOrUpdateMotd(motd);
 	}
 
+	@CacheEvict(cacheNames = "motds", key = "#motd.audience.concat(#motd.sessionkey)")
 	private Motd createOrUpdateMotd(final Motd motd) {
 		if (motd.getMotdkey() != null) {
 			Motd oldMotd = motdRepository.findByKey(motd.getMotdkey());
@@ -160,11 +171,20 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 			}
 		}
 
+		if (null != motd.getId()) {
+			Motd oldMotd = get(motd.getId());
+			motd.setMotdkey(oldMotd.getMotdkey());
+		} else {
+			motd.setMotdkey(sessionService.generateKey());
+		}
+		save(motd);
+
 		return motdRepository.save(motd);
 	}
 
 	@Override
 	@PreAuthorize("isAuthenticated() and hasPermission(1,'motd','admin')")
+	@CacheEvict(cacheNames = "motds", key = "#motd.audience.concat(#motd.sessionkey)")
 	public void delete(Motd motd) {
 		motdRepository.delete(motd);
 	}
@@ -177,6 +197,7 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@Cacheable(cacheNames = "motdlist", key = "#username")
 	public MotdList getMotdListByUsername(final String username) {
 		final User user = userService.getCurrentUser();
 		if (username.equals(user.getUsername()) && !"guest".equals(user.getType())) {
@@ -187,6 +208,7 @@ public class MotdServiceImpl extends EntityService<Motd> implements MotdService
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@CachePut(cacheNames = "motdlist", key = "#motdList.username")
 	public MotdList saveMotdList(MotdList motdList) {
 		final User user = userService.getCurrentUser();
 		if (user.getUsername().equals(motdList.getUsername())) {
diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java
index 1aaba3134..58f687791 100644
--- a/src/main/java/de/thm/arsnova/services/SessionService.java
+++ b/src/main/java/de/thm/arsnova/services/SessionService.java
@@ -66,7 +66,7 @@ public interface SessionService {
 
 	Session updateInternal(Session session, User user);
 
-	void delete(String sessionkey);
+	int[] deleteCascading(Session session);
 
 	ScoreStatistics getLearningProgress(String sessionkey, String type, String questionVariant);
 
diff --git a/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java b/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java
index bfc3532d8..dd0194392 100644
--- a/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java
@@ -17,6 +17,10 @@
  */
 package de.thm.arsnova.services;
 
+import de.thm.arsnova.persistance.AnswerRepository;
+import de.thm.arsnova.persistance.CommentRepository;
+import de.thm.arsnova.persistance.ContentRepository;
+import de.thm.arsnova.persistance.LogEntryRepository;
 import de.thm.arsnova.util.ImageUtils;
 import de.thm.arsnova.connector.client.ConnectorClient;
 import de.thm.arsnova.connector.model.Course;
@@ -42,11 +46,15 @@ import de.thm.arsnova.exceptions.PayloadTooLargeException;
 import de.thm.arsnova.exceptions.UnauthorizedException;
 import de.thm.arsnova.persistance.SessionRepository;
 import de.thm.arsnova.persistance.VisitedSessionRepository;
+import org.ektorp.UpdateConflictException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Caching;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.ApplicationEventPublisherAware;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@@ -68,8 +76,16 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 
 	private static final Logger logger = LoggerFactory.getLogger(SessionServiceImpl.class);
 
+	private LogEntryRepository dbLogger;
+
 	private SessionRepository sessionRepository;
 
+	private ContentRepository contentRepository;
+
+	private AnswerRepository answerRepository;
+
+	private CommentRepository commentRepository;
+
 	private VisitedSessionRepository visitedSessionRepository;
 
 	private UserService userService;
@@ -92,7 +108,11 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 
 	public SessionServiceImpl(
 			SessionRepository repository,
+			ContentRepository contentRepository,
+			AnswerRepository answerRepository,
+			CommentRepository commentRepository,
 			VisitedSessionRepository visitedSessionRepository,
+			LogEntryRepository dbLogger,
 			UserService userService,
 			FeedbackService feedbackService,
 			ScoreCalculatorFactory scoreCalculatorFactory,
@@ -100,7 +120,11 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 			@Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
 		super(Session.class, repository, jackson2HttpMessageConverter.getObjectMapper());
 		this.sessionRepository = repository;
+		this.contentRepository = contentRepository;
+		this.answerRepository = answerRepository;
+		this.commentRepository = commentRepository;
 		this.visitedSessionRepository = visitedSessionRepository;
+		this.dbLogger = dbLogger;
 		this.userService = userService;
 		this.feedbackService = feedbackService;
 		this.scoreCalculatorFactory = scoreCalculatorFactory;
@@ -154,7 +178,23 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 			logger.info("Delete inactive sessions.");
 			long unixTime = System.currentTimeMillis();
 			long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L;
-			sessionRepository.deleteInactiveGuestSessions(lastActivityBefore);
+			int totalCount[] = new int[] {0, 0, 0};
+			List<Session> inactiveSessions = sessionRepository.findInactiveGuestSessionsMetadata(lastActivityBefore);
+			for (Session session : inactiveSessions) {
+				int[] count = deleteCascading(session);
+				totalCount[0] += count[0];
+				totalCount[1] += count[1];
+				totalCount[2] += count[2];
+			}
+
+			if (!inactiveSessions.isEmpty()) {
+				logger.info("Deleted {} inactive guest sessions.", inactiveSessions.size());
+				dbLogger.log("cleanup", "type", "session",
+						"sessionCount", inactiveSessions.size(),
+						"questionCount", totalCount[1],
+						"answerCount", totalCount[2],
+						"commentCount", totalCount[3]);
+			}
 		}
 	}
 
@@ -183,7 +223,7 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		userService.addUserToSessionBySocketId(socketId, keyword);
 
 		if (session.getCreator().equals(user.getUsername())) {
-			sessionRepository.updateSessionOwnerActivity(session);
+			updateSessionOwnerActivity(session);
 		}
 		sessionRepository.registerAsOnlineUser(user, session);
 
@@ -197,6 +237,24 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		return session;
 	}
 
+	@CachePut(value = "sessions")
+	private Session updateSessionOwnerActivity(final Session session) {
+		try {
+			/* Do not clutter CouchDB. Only update once every 3 hours. */
+			if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) {
+				return session;
+			}
+
+			session.setLastOwnerActivity(System.currentTimeMillis());
+			save(session);
+
+			return session;
+		} catch (final UpdateConflictException e) {
+			logger.error("Failed to update lastOwnerActivity for session {}.", session, e);
+			return session;
+		}
+	}
+
 	@Override
 	@PreAuthorize("isAuthenticated()")
 	public Session getByKey(final String keyword) {
@@ -286,6 +344,7 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 
 	@Override
 	@PreAuthorize("isAuthenticated()")
+	@Caching(evict = @CacheEvict(cacheNames = "sessions", key = "#result.keyword"))
 	public Session save(final Session session) {
 		if (connectorClient != null && session.getCourseId() != null) {
 			if (!connectorClient.getMembership(
@@ -310,14 +369,19 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		sf.setPi(true);
 		session.setFeatures(sf);
 
-		final Session result = sessionRepository.save(userService.getCurrentUser(), session);
+		session.setKeyword(generateKey());
+		session.setCreator(userService.getCurrentUser().getUsername());
+		session.setActive(true);
+		session.setFeedbackLock(false);
+
+		final Session result = save(session);
 		this.publisher.publishEvent(new NewSessionEvent(this, result));
 		return result;
 	}
 
 	@Override
 	public boolean isKeyAvailable(final String keyword) {
-		return sessionRepository.sessionKeyAvailable(keyword);
+		return getByKey(keyword) == null;
 	}
 
 	@Override
@@ -356,13 +420,14 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		}
 		session.setActive(lock);
 		this.publisher.publishEvent(new StatusSessionEvent(this, session));
-		sessionRepository.update(session);
+		sessionRepository.save(session);
 
 		return session;
 	}
 
 	@Override
 	@PreAuthorize("isAuthenticated() and hasPermission(#session, 'owner')")
+	@CachePut(value = "sessions", key = "#session")
 	public Session update(final String sessionkey, final Session session) {
 		final Session existingSession = sessionRepository.findByKeyword(sessionkey);
 
@@ -384,19 +449,24 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		handleLogo(session);
 		existingSession.setPpLogo(session.getPpLogo());
 
-		sessionRepository.update(existingSession);
+		sessionRepository.save(existingSession);
 
 		return session;
 	}
 
 	@Override
-	@PreAuthorize("isAuthenticated() and hasPermission(1,'motd','admin')")
+	@PreAuthorize("isAuthenticated() and hasPermission(1, 'motd', 'admin')")
+	@Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#sessionkey.keyword") })
 	public Session updateCreator(String sessionkey, String newCreator) {
-		final Session existingSession = sessionRepository.findByKeyword(sessionkey);
-		if (existingSession == null) {
+		final Session session = sessionRepository.findByKeyword(sessionkey);
+		if (session == null) {
 			throw new NullPointerException("Could not load session " + sessionkey + ".");
 		}
-		return sessionRepository.changeSessionCreator(existingSession, newCreator);
+
+		session.setCreator(newCreator);
+		save(session);
+
+		return save(session);
 	}
 
 	/*
@@ -406,20 +476,28 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 	@Override
 	public Session updateInternal(final Session session, final User user) {
 		if (session.isCreator(user)) {
-			sessionRepository.update(session);
+			sessionRepository.save(session);
 			return session;
 		}
 		return null;
 	}
 
 	@Override
-	@PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')")
-	public void delete(final String sessionkey) {
-		final Session session = sessionRepository.findByKeyword(sessionkey);
-
-		sessionRepository.deleteSession(session);
+	@PreAuthorize("isAuthenticated() and hasPermission(#session, 'owner')")
+	@CacheEvict("sessions")
+	public int[] deleteCascading(final Session session) {
+		int[] count = new int[] {0, 0, 0};
+		List<String> contentIds = contentRepository.findIdsBySessionId(session.getId());
+		count[2] = commentRepository.deleteBySessionId(session.getId());
+		count[1] = answerRepository.deleteByContentIds(contentIds);
+		count[0] = contentRepository.deleteBySessionId(session.getId());
+		sessionRepository.delete(session);
+		logger.debug("Deleted session document {} and related data.", session.getId());
+		dbLogger.log("delete", "type", "session", "id", session.getId());
 
 		this.publisher.publishEvent(new DeleteSessionEvent(this, session));
+
+		return count;
 	}
 
 	@Override
@@ -485,7 +563,7 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		}
 		session.setFeatures(features);
 		this.publisher.publishEvent(new FeatureChangeEvent(this, session));
-		sessionRepository.update(session);
+		sessionRepository.save(session);
 
 		return session.getFeatures();
 	}
@@ -503,7 +581,7 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 
 		session.setFeedbackLock(lock);
 		this.publisher.publishEvent(new LockFeedbackEvent(this, session));
-		sessionRepository.update(session);
+		sessionRepository.save(session);
 
 		return session.getFeedbackLock();
 	}
@@ -517,7 +595,7 @@ public class SessionServiceImpl extends EntityService<Session> implements Sessio
 		}
 		session.setFlipFlashcards(flip);
 		this.publisher.publishEvent(new FlipFlashcardsEvent(this, session));
-		sessionRepository.update(session);
+		sessionRepository.save(session);
 
 		return session.getFlipFlashcards();
 	}
diff --git a/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java b/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java
index 7345ec3b6..0d57a8443 100644
--- a/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java
+++ b/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java
@@ -19,6 +19,7 @@ package de.thm.arsnova.services;
 
 import de.thm.arsnova.entities.Statistics;
 import de.thm.arsnova.persistance.StatisticsRepository;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
@@ -43,7 +44,12 @@ public class StatisticsServiceImpl implements StatisticsService {
 
 	@Scheduled(initialDelay = 0, fixedRate = 10000)
 	private void refreshStatistics() {
-		statistics = statisticsRepository.getStatistics();
+		statistics = loadStatistics();
+	}
+
+	@Cacheable("statistics")
+	private Statistics loadStatistics() {
+		return statisticsRepository.getStatistics();
 	}
 
 	@Override
diff --git a/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java b/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java
index 3fb7266b1..269b173c2 100644
--- a/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java
+++ b/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java
@@ -21,6 +21,7 @@ import de.thm.arsnova.entities.Session;
 import de.thm.arsnova.entities.User;
 import de.thm.arsnova.entities.transport.ScoreStatistics;
 import de.thm.arsnova.persistance.SessionStatisticsRepository;
+import org.springframework.cache.annotation.Cacheable;
 
 /**
  * Base class for the score feature that allows filtering on the question variant.
@@ -37,7 +38,12 @@ abstract class VariantScoreCalculator implements ScoreCalculator {
 		this.sessionStatisticsRepository = sessionStatisticsRepository;
 	}
 
-	private void loadProgress(final Session session) {
+	@Cacheable("learningprogress")
+	private Score loadProgress(final Session session) {
+		return sessionStatisticsRepository.getLearningProgress(session);
+	}
+
+	private void refreshProgress(final Session session) {
 		this.courseScore = sessionStatisticsRepository.getLearningProgress(session);
 	}
 
@@ -47,7 +53,7 @@ abstract class VariantScoreCalculator implements ScoreCalculator {
 
 	@Override
 	public ScoreStatistics getCourseProgress(Session session) {
-		this.loadProgress(session);
+		this.refreshProgress(session);
 		this.filterVariant();
 		return this.createCourseProgress();
 	}
@@ -56,7 +62,7 @@ abstract class VariantScoreCalculator implements ScoreCalculator {
 
 	@Override
 	public ScoreStatistics getMyProgress(Session session, User user) {
-		this.loadProgress(session);
+		this.refreshProgress(session);
 		this.filterVariant();
 		return this.createMyProgress(user);
 	}
diff --git a/src/site/markdown/development/caching.md b/src/site/markdown/development/caching.md
index 108591d71..76ed0623a 100644
--- a/src/site/markdown/development/caching.md
+++ b/src/site/markdown/development/caching.md
@@ -52,7 +52,7 @@ Cache name | Key | Description
 `flashcardquestions` | database id of session | Contains all "flashcard" variant questions for the specified session.
 `questions` | `Question` entity | Contains single question objects.
 `questions` | database id of question | Although it shares the name of the previously mentioned cache, it is in essence a different cache because the keys are different. This means that the same `Question` object might be associated with two different keys.
-`answers`| `Question` entity | Contains single answer objects.
+`answers`| database id of question | Contains single answer objects.
 `learningprogress` | `Session` entity | Contains `CourseScore` objects to calculate the learning progress values for the specified session.
 `sessions` | keyword of session | Contains sessions identified by their keywords.
 `sessions` | database id of session | Although it shares the name of the previously mentioned cache, it is in essence a different cache because the keys are different. This means that the same `Session` object might be associated with two different keys.
-- 
GitLab