diff --git a/pom.xml b/pom.xml
index 8a0a68ab6a06673474bc17449ad6fd350d96775a..4214ccafcb62dc81aa32c23a632d48a91c6f6d99 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,11 @@
             <artifactId>kypo2-security-commons</artifactId>
             <version>1.0.40</version>
         </dependency>
+        <dependency>
+            <groupId>cz.muni.ics.kypo</groupId>
+            <artifactId>kypo-elasticsearch-documents</artifactId>
+            <version>1.0.16</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
@@ -114,6 +119,12 @@
             <artifactId>springfox-swagger-ui</artifactId>
             <version>${swagger.version}</version>
         </dependency>
+        <!--RandomStringUtils-->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.11</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java
index 983d9e46babbcac857704ecdccaed7ead472194d..7977ea73778de3161da023e0ea6f6db09ba143d9 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java
@@ -1,12 +1,18 @@
 package cz.muni.ics.kypo.training.adaptive;
 
 import cz.muni.ics.kypo.commons.startup.config.MicroserviceRegistrationConfiguration;
+import cz.muni.ics.kypo.training.adaptive.config.ObjectMappersConfiguration;
+import cz.muni.ics.kypo.training.adaptive.config.ValidationMessagesConfig;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.annotation.Import;
 
 @SpringBootApplication
-@Import(value = {MicroserviceRegistrationConfiguration.class})
+@Import(value = {
+        MicroserviceRegistrationConfiguration.class,
+        ValidationMessagesConfig.class,
+        ObjectMappersConfiguration.class
+})
 public class DemoApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..5cd104fe606a8360ddb8b75676a516736d1b9f52
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java
@@ -0,0 +1,39 @@
+package cz.muni.ics.kypo.training.adaptive.config;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.BeanDeserializer;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
+
+import javax.validation.*;
+import java.io.IOException;
+import java.util.Set;
+
+public class BeanValidationDeserializer extends BeanDeserializer {
+
+    private final static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+    private final Validator validator = factory.getValidator();
+
+    public BeanValidationDeserializer(BeanDeserializerBase src) {
+        super(src);
+    }
+
+    @Override
+    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        Object instance = super.deserialize(p, ctxt);
+        validate(instance);
+        return instance;
+    }
+
+    private void validate(Object instance) {
+        Set<ConstraintViolation<Object>> violations = validator.validate(instance);
+        if (!violations.isEmpty()) {
+            StringBuilder msg = new StringBuilder();
+            msg.append("JSON object is not valid. Reasons (").append(violations.size()).append("): ");
+            for (ConstraintViolation<Object> violation : violations) {
+                msg.append(violation.getMessage()).append(", ");
+            }
+            throw new ConstraintViolationException(msg.toString(), violations);
+        }
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..9bf57dc2d2abc6f743c2176d19b5e7030785e0d9
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java
@@ -0,0 +1,81 @@
+package cz.muni.ics.kypo.training.adaptive.config;
+
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.BeanDeserializer;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * The type Object mapper config elasticsearch.
+ */
+@Configuration
+public class ObjectMappersConfiguration {
+    /**
+     * General object mapper bean.
+     *
+     * @return the object mapper
+     */
+    @Bean
+    @Primary
+    public ObjectMapper objectMapper() {
+        return initializeBasicObjectMapperConfig();
+    }
+
+    /**
+     * Object mapper bean used in restTemplate calls. Basically, it contains validator settings to validate object in deserialization phase using Bean Validations.
+     *
+     * @return the object mapper
+     */
+    @Bean
+    @Qualifier("webClientObjectMapper")
+    public ObjectMapper webClientObjectMapper() {
+        ObjectMapper objectMapper = initializeBasicObjectMapperConfig();
+        objectMapper.registerModule(getSimpleValidationModule());
+        return objectMapper;
+    }
+
+    /**
+     * Object mapper object mapper.
+     *
+     * @return the object mapper
+     */
+    @Bean("objMapperForElasticsearch")
+    public ObjectMapper elasticsearchObjectMapper() {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+        objectMapper.registerModule(new JavaTimeModule());
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        return objectMapper;
+    }
+
+    private ObjectMapper initializeBasicObjectMapperConfig() {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+        objectMapper.registerModule(new JavaTimeModule());
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+        return objectMapper;
+    }
+
+    private SimpleModule getSimpleValidationModule() {
+        SimpleModule validationModule = new SimpleModule();
+        validationModule.setDeserializerModifier(new BeanDeserializerModifier() {
+            @Override
+            public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
+                if (deserializer instanceof BeanDeserializer) {
+                    return new BeanValidationDeserializer((BeanDeserializer) deserializer);
+                }
+                return deserializer;
+            }
+        });
+        return validationModule;
+    }
+
+
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fb95d16639dbba438a85be5098c846427b2a167
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java
@@ -0,0 +1,167 @@
+package cz.muni.ics.kypo.training.adaptive.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.JavaApiError;
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.PythonApiError;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+
+/**
+ * The type Web client config.
+ */
+@Import(ObjectMappersConfiguration.class)
+@Configuration
+public class WebClientConfig {
+
+
+    @Value("${openstack-server.uri}")
+    private String kypoOpenStackURI;
+    @Value("${user-and-group-server.uri}")
+    private String userAndGroupURI;
+    @Value("${elasticsearch-service.uri}")
+    private String elasticsearchServiceURI;
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public WebClientConfig(@Qualifier("webClientObjectMapper") ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    /**
+     * Openstack service web client web client.
+     *
+     * @return the web client
+     */
+    @Bean
+    @Qualifier("sandboxServiceWebClient")
+    public WebClient sandboxServiceWebClient() {
+        return WebClient.builder()
+                .baseUrl(kypoOpenStackURI)
+                .defaultHeaders(headers -> {
+                    headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+                    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+                })
+                .filters(exchangeFilterFunctions -> {
+                    exchangeFilterFunctions.add(addSecurityHeader());
+                    exchangeFilterFunctions.add(openStackSandboxServiceExceptionHandlingFunction());
+                })
+                .build();
+    }
+
+    /**
+     * User management service web client web client.
+     *
+     * @return the web client
+     */
+    @Bean
+    @Qualifier("userManagementServiceWebClient")
+    public WebClient userManagementServiceWebClient() {
+        return WebClient.builder()
+                .baseUrl(userAndGroupURI)
+                .defaultHeaders(headers -> {
+                    headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+                    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+                })
+                .filters(exchangeFilterFunctions -> {
+                    exchangeFilterFunctions.add(addSecurityHeader());
+                    exchangeFilterFunctions.add(javaMicroserviceExceptionHandlingFunction());
+                })
+                .build();
+    }
+
+    /**
+     * Elasticsearch service web client.
+     *
+     * @return the web client
+     */
+    @Bean
+    @Qualifier("elasticsearchServiceWebClient")
+    public WebClient elasticsearchServiceWebClient() {
+        return WebClient.builder()
+                .baseUrl(elasticsearchServiceURI)
+                .defaultHeaders(headers -> {
+                    headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+                    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+                })
+                .filters(exchangeFilterFunctions -> {
+                    exchangeFilterFunctions.add(addSecurityHeader());
+                    exchangeFilterFunctions.add(javaMicroserviceExceptionHandlingFunction());
+                })
+                .build();
+    }
+
+    private ExchangeFilterFunction addSecurityHeader() {
+        return (request, next) -> {
+            OAuth2Authentication authenticatedUser = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
+            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authenticatedUser.getDetails();
+            ClientRequest filtered = ClientRequest.from(request)
+                    .header("Authorization", "Bearer " + details.getTokenValue())
+                    .build();
+            return next.exchange(filtered);
+        };
+    }
+
+    private ExchangeFilterFunction openStackSandboxServiceExceptionHandlingFunction() {
+        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
+            if(clientResponse.statusCode().is4xxClientError() || clientResponse.statusCode().is5xxServerError()) {
+                return clientResponse.bodyToMono(String.class)
+                    .flatMap(errorBody -> {
+                        if (errorBody == null || errorBody.isBlank()) {
+                            throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode());
+                        }
+                        PythonApiError pythonApiError = null;
+                        try {
+                            pythonApiError = objectMapper.readValue(errorBody, PythonApiError.class);
+                            pythonApiError.setStatus(clientResponse.statusCode());
+                        } catch (IOException e) {
+                            throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode());
+                        }
+                        throw new CustomWebClientException(pythonApiError);
+                    });
+            } else {
+                return Mono.just(clientResponse);
+            }
+        });
+    }
+
+    private ExchangeFilterFunction javaMicroserviceExceptionHandlingFunction() {
+        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
+            if(clientResponse.statusCode().is4xxClientError() || clientResponse.statusCode().is5xxServerError()) {
+                return clientResponse.bodyToMono(String.class)
+                    .flatMap(errorBody -> {
+                        if (errorBody == null || errorBody.isBlank()) {
+                            throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode());
+                        }
+                        JavaApiError javaApiError = null;
+                        try {
+                            javaApiError = objectMapper.readValue(errorBody, JavaApiError.class);
+                            javaApiError.setStatus(clientResponse.statusCode());
+                        } catch (IOException e) {
+                            throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode());
+                        }
+                        throw new CustomWebClientException(javaApiError);
+                    });
+            } else {
+                return Mono.just(clientResponse);
+            }
+        });
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java
index 0ea3b0079b55ffd814c8cfe8551ea2fc25861c4b..b02ce1dcf6f7c631c771a0cdcf531c10d9abd3a9 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java
@@ -1,15 +1,9 @@
 package cz.muni.ics.kypo.training.adaptive.domain.phases;
 
 
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Inheritance;
-import javax.persistence.InheritanceType;
-import javax.persistence.SequenceGenerator;
-import javax.persistence.Table;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+
+import javax.persistence.*;
 import java.io.Serializable;
 
 
@@ -31,7 +25,9 @@ public abstract class AbstractPhase implements Serializable {
     @Column(name = "order_in_training_definition", nullable = false)
     private Integer order;
 
-    private Long trainingDefinitionId;
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "training_definition_id")
+    private TrainingDefinition trainingDefinition;
 
     public String getTitle() {
         return title;
@@ -57,12 +53,12 @@ public abstract class AbstractPhase implements Serializable {
         this.id = id;
     }
 
-    public Long getTrainingDefinitionId() {
-        return trainingDefinitionId;
+    public TrainingDefinition getTrainingDefinition() {
+        return trainingDefinition;
     }
 
-    public void setTrainingDefinitionId(Long trainingDefinition) {
-        this.trainingDefinitionId = trainingDefinition;
+    public void setTrainingDefinition(TrainingDefinition trainingDefinition) {
+        this.trainingDefinition = trainingDefinition;
     }
 
     @Override
@@ -71,7 +67,7 @@ public abstract class AbstractPhase implements Serializable {
                 "id=" + id +
                 ", title='" + title + '\'' +
                 ", order=" + order +
-                ", trainingDefinitionId=" + trainingDefinitionId +
+                ", trainingDefinitionId=" + trainingDefinition +
                 '}';
     }
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java
index 4006b1a38e16405d0b528c81f0dc5d8876a7a0aa..45f926ade1e5b84097b7ff876b39e01b2ba78b20 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java
@@ -5,6 +5,7 @@ import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.AbstractPhaseImport
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 
+import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 import java.util.ArrayList;
@@ -32,6 +33,7 @@ public class ImportTrainingDefinitionDTO {
 	@ApiModelProperty(value = "Sign if stepper bar should be displayed.", example = "false")
 	@NotNull(message = "{trainingdefinitionimport.showStepperBar.NotNull.message}")
 	private boolean showStepperBar;
+	@Valid
 	@ApiModelProperty(value = "Information about all levels in training definition.")
 	private List<AbstractPhaseImportDTO> phases = new ArrayList<>();
 	@ApiModelProperty(value = "Estimated time it takes to finish runs created from this definition.", example = "5")
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java
new file mode 100644
index 0000000000000000000000000000000000000000..49983cb00caaa12fbd321ba385923b2d12744cd5
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java
@@ -0,0 +1,25 @@
+package cz.muni.ics.kypo.training.adaptive.enums;
+
+/**
+ * The enumeration of user roles.
+ *
+ */
+public enum RoleType {
+
+    /**
+     * Role training administrator role.
+     */
+    ROLE_TRAINING_ADMINISTRATOR,
+    /**
+     * Role training designer role.
+     */
+    ROLE_TRAINING_DESIGNER,
+    /**
+     * Role training organizer role.
+     */
+    ROLE_TRAINING_ORGANIZER,
+    /**
+     * Role training trainee role.
+     */
+    ROLE_TRAINING_TRAINEE;
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c43ee11088d1c41f2141650020d34b2f80954d5
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java
@@ -0,0 +1,41 @@
+package cz.muni.ics.kypo.training.adaptive.exceptions;
+
+/**
+ * The type Elasticsearch training service layer exception.
+ */
+public class ElasticsearchTrainingServiceLayerException extends RuntimeException {
+
+    /**
+     * Instantiates a new Elasticsearch training service layer exception.
+     */
+    public ElasticsearchTrainingServiceLayerException() {
+    }
+
+    /**
+     * Instantiates a new Elasticsearch training service layer exception.
+     *
+     * @param message the message
+     */
+    public ElasticsearchTrainingServiceLayerException(String message) {
+        super(message);
+    }
+
+    /**
+     * Instantiates a new Elasticsearch training service layer exception.
+     *
+     * @param message the message
+     * @param ex      the exception
+     */
+    public ElasticsearchTrainingServiceLayerException(String message, Throwable ex) {
+        super(message, ex);
+    }
+
+    /**
+     * Instantiates a new Elasticsearch training service layer exception.
+     *
+     * @param ex the exception
+     */
+    public ElasticsearchTrainingServiceLayerException(Throwable ex) {
+        super(ex);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java
index 116b26e4cdd08650e3da07ac2a4ce7ab99c5492b..0acc8226734f40c1b063a35062ffc7581240fca2 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java
@@ -10,8 +10,6 @@ import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.Quest
 import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.QuestionImportDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionChoiceDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionDTO;
-import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionUpdateDTO;
-import cz.muni.ics.kypo.training.adaptive.dto.responses.PageResultResource;
 import org.mapstruct.Mapper;
 import org.mapstruct.ReportingPolicy;
 import org.springframework.data.domain.Page;
@@ -28,7 +26,6 @@ import java.util.*;
 public interface QuestionMapper extends ParentMapper {
 //    QUESTIONS
     Question mapToEntity(QuestionDTO dto);
-    Question mapToEntity(QuestionUpdateDTO dto);
     Question mapToEntity(QuestionImportDTO dto);
 
     QuestionArchiveDTO mapToQuestionArchiveDTO(Question entity);
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java
index e2e274b580304414bd4737e41bae35c5f75cd20c..57828a7a83178a5af21f03ac3ec254f882d383fb 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java
@@ -7,26 +7,28 @@ import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;
 
+import javax.persistence.NamedQuery;
 import java.util.List;
+import java.util.Optional;
 
 @Repository
 public interface AbstractPhaseRepository extends JpaRepository<AbstractPhase, Long> {
 
     List<AbstractPhase> findAllByTrainingDefinitionIdOrderByOrder(long trainingDefinitionId);
 
-    @Query("SELECT COALESCE(MAX(l.order), -1) FROM AbstractPhase l WHERE l.trainingDefinitionId = :trainingDefinitionId")
+    @Query("SELECT COALESCE(MAX(l.order), -1) FROM AbstractPhase l WHERE l.trainingDefinition = :trainingDefinitionId")
     Integer getCurrentMaxOrder(@Param("trainingDefinitionId") Long trainingDefinitionId);
 
     @Modifying
     @Query("UPDATE AbstractPhase l SET l.order = l.order - 1 " +
-            "WHERE l.trainingDefinitionId = :trainingDefinitionId " +
+            "WHERE l.trainingDefinition = :trainingDefinitionId " +
             "AND l.order > :order ")
     void decreaseOrderAfterPhaseWasDeleted(@Param("trainingDefinitionId") Long trainingDefinitionId,
                                            @Param("order") int order);
 
     @Modifying
     @Query("UPDATE AbstractPhase l SET l.order = l.order + 1 " +
-            "WHERE l.trainingDefinitionId = :trainingDefinitionId " +
+            "WHERE l.trainingDefinition = :trainingDefinitionId " +
             "AND l.order >= :lowerBound " +
             "AND l.order < :upperBound ")
     void increaseOrderOfPhasesOnInterval(@Param("trainingDefinitionId") Long trainingDefinitionId,
@@ -35,11 +37,23 @@ public interface AbstractPhaseRepository extends JpaRepository<AbstractPhase, Lo
 
     @Modifying
     @Query("UPDATE AbstractPhase l SET l.order = l.order - 1 " +
-            "WHERE l.trainingDefinitionId = :trainingDefinitionId " +
+            "WHERE l.trainingDefinition = :trainingDefinitionId " +
             "AND l.order > :lowerBound " +
             "AND l.order <= :upperBound ")
     void decreaseOrderOfPhasesOnInterval(@Param("trainingDefinitionId") Long trainingDefinitionId,
                                          @Param("lowerBound") int lowerBound,
                                          @Param("upperBound") int upperBound);
 
+    @Query("SELECT ap FROM AbstractPhase ap WHERE l.trainingDefinition.id = :trainingDefinitionId AND ap.id = :phaseId")
+    Optional<AbstractPhase> findPhaseInDefinition(@Param("trainingDefinitionId") Long trainingDefinitionId,
+                                                  @Param("phaseId") Long phaseId);
+
+    @Query("SELECT ap FROM AbstractPhase ap " +
+                    "JOIN FETCH ap.trainingDefinition td " +
+                    "JOIN FETCH td.authors " +
+                    "WHERE ap.id = :phaseId")
+    Optional<AbstractPhase> findByIdWithDefinition(@Param("phaseId") Long phaseId);
+
+    @Query("SELECT ap FROM AbstractPhase ap WHERE ap.trainingDefinition.id = :trainingDefinitionId AND ap.order = 0")
+    Optional<AbstractPhase> findFirstPhaseOfTrainingDefinition(@Param("trainingDefinitionId") Long trainingDefinitionId);
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java
index f1023a478aba48a01cbfab56c7e14fd39f379382..32cb2f1a6ded7ab3c3ff882321e05fc4d994c8ab 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java
@@ -11,7 +11,7 @@ import java.util.List;
 @Repository
 public interface TrainingPhaseRepository extends JpaRepository<TrainingPhase, Long> {
 
-    @Query("SELECT COUNT(p.id) FROM TrainingPhase p WHERE p.trainingDefinitionId = :trainingDefinitionId")
+    @Query("SELECT COUNT(p.id) FROM TrainingPhase p WHERE p.trainingDefinition = :trainingDefinitionId")
     int getNumberOfExistingPhases(@Param("trainingDefinitionId") Long trainingDefinitionId);
 
     List<TrainingPhase> findAllByTrainingDefinitionIdOrderByOrder(Long trainingDefinitionId);
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java
index 76a3a16d97a3e1ef21d3e024fa0e6af4c2aed5a0..e6bd126e33d9fc9f3961b49600da701497ee5660 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java
@@ -104,12 +104,12 @@ public interface TrainingRunRepository extends JpaRepository<TrainingRun, Long>,
     Page<TrainingRun> findAllByParticipantRefId(@Param("userRefId") Long userRefId, Pageable pageable);
 
     /**
-     * Find training run by id including current level
+     * Find training run by id including current phase
      *
      * @param trainingRunId the training run id
      * @return {@link TrainingRun} including {@link AbstractPhase}
      */
-    Optional<TrainingRun> findByIdWithLevel(@Param("trainingRunId") Long trainingRunId);
+    Optional<TrainingRun> findByIdWithPhase(@Param("trainingRunId") Long trainingRunId);
 
     /**
      * Find all training runs by id of associated training definition that are accessible to participant by user ref id.
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java
index e26b95796953f3c138fd22c9f958570cd6c83729..7bd5508c0e9c22bc55174310651ffe1a01e18853 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java
@@ -1,55 +1,112 @@
 package cz.muni.ics.kypo.training.adaptive.service;
 
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
 import cz.muni.ics.kypo.training.adaptive.dto.info.InfoPhaseDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.info.InfoPhaseUpdateDTO;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
 import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND;
+import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED;
+
 @Service
 public class InfoPhaseService {
 
     private static final Logger LOG = LoggerFactory.getLogger(InfoPhaseService.class);
 
+    private final TrainingInstanceRepository trainingInstanceRepository;
+    private final TrainingDefinitionRepository trainingDefinitionRepository;
     private final InfoPhaseRepository infoPhaseRepository;
     private final AbstractPhaseRepository abstractPhaseRepository;
 
     @Autowired
-    public InfoPhaseService(InfoPhaseRepository infoPhaseRepository, AbstractPhaseRepository abstractPhaseRepository) {
+    public InfoPhaseService(TrainingDefinitionRepository trainingDefinitionRepository,
+                            TrainingInstanceRepository trainingInstanceRepository,
+                            InfoPhaseRepository infoPhaseRepository,
+                            AbstractPhaseRepository abstractPhaseRepository) {
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
         this.infoPhaseRepository = infoPhaseRepository;
         this.abstractPhaseRepository = abstractPhaseRepository;
     }
 
     public InfoPhaseDTO createDefaultInfoPhase(Long trainingDefinitionId) {
+        TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+
         InfoPhase infoPhase = new InfoPhase();
         infoPhase.setContent("Content of info phase");
         infoPhase.setTitle("Title of info phase");
-        infoPhase.setTrainingDefinitionId(trainingDefinitionId);
+        infoPhase.setTrainingDefinition(trainingDefinition);
         infoPhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1);
 
         InfoPhase persistedEntity = infoPhaseRepository.save(infoPhase);
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
         return BeanMapper.INSTANCE.toDto(persistedEntity);
     }
 
-    public InfoPhaseDTO updateInfoPhase(Long definitionId, Long phaseId, InfoPhaseUpdateDTO infoPhaseUpdateDto) {
-        InfoPhase infoPhaseUpdate = BeanMapper.INSTANCE.toEntity(infoPhaseUpdateDto);
-        infoPhaseUpdate.setId(phaseId);
-        InfoPhase persistedInfoPhase = infoPhaseRepository.findById(infoPhaseUpdate.getId())
-                .orElseThrow(() -> new RuntimeException("Info phase was not found"));
-        // TODO throw proper exception once kypo2-training is migrated
+    public InfoPhaseDTO updateInfoPhase(Long definitionId, Long phaseId, InfoPhase infoPhaseToUpdate) {
+        TrainingDefinition trainingDefinition = findDefinitionById(definitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+        if (!existPhaseInDefinition(trainingDefinition, infoPhaseToUpdate.getId())) {
+            throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", infoPhaseToUpdate.getId().getClass(),
+                    infoPhaseToUpdate.getId(), "Phase was not found in definition (id: " + definitionId + ")."));
+        }
+        infoPhaseToUpdate.setId(phaseId);
+        InfoPhase persistedInfoPhase = findInfoPhaseById(phaseId);
+        infoPhaseToUpdate.setTrainingDefinition(persistedInfoPhase.getTrainingDefinition());
+        infoPhaseToUpdate.setOrder(persistedInfoPhase.getOrder());
 
-        // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed)
+        InfoPhase savedEntity = infoPhaseRepository.save(infoPhaseToUpdate);
+        return BeanMapper.INSTANCE.toDto(savedEntity);
+    }
 
-        infoPhaseUpdate.setTrainingDefinitionId(persistedInfoPhase.getTrainingDefinitionId());
-        infoPhaseUpdate.setOrder(persistedInfoPhase.getOrder());
+    private InfoPhase findInfoPhaseById(Long phaseId) {
+        return infoPhaseRepository.findById(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
 
-        InfoPhase savedEntity = infoPhaseRepository.save(infoPhaseUpdate);
-        return BeanMapper.INSTANCE.toDto(savedEntity);
+    private TrainingDefinition findDefinitionById(Long id) {
+        return trainingDefinitionRepository.findById(id)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id)));
+    }
+
+    private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) {
+        if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    ARCHIVED_OR_RELEASED));
+        }
+        if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    "Cannot update training definition with already created training instance. " +
+                            "Remove training instance/s before updating training definition."));
+        }
+    }
+
+    private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) {
+        return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId)
+                .isPresent();
+    }
+
+    private LocalDateTime getCurrentTimeInUTC() {
+        return LocalDateTime.now(Clock.systemUTC());
     }
 
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java
index 70753bc9e7e3f5a4f1fb7ffbe7666a42e87ac7af..3f88010d511dff04885480201712b5d0a82904a7 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java
@@ -1,39 +1,72 @@
 package cz.muni.ics.kypo.training.adaptive.service;
 
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
 import cz.muni.ics.kypo.training.adaptive.dto.AbstractPhaseDTO;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
 import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.Clock;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
 @Transactional
 public class PhaseService {
 
+    public static final String PHASE_NOT_FOUND = "Phase not found.";
+
+
     private final AbstractPhaseRepository abstractPhaseRepository;
     private final TrainingPhaseService trainingPhaseService;
+    private final TrainingDefinitionRepository trainingDefinitionRepository;
 
     @Autowired
-    public PhaseService(AbstractPhaseRepository abstractPhaseRepository, TrainingPhaseService trainingPhaseService) {
+    public PhaseService(AbstractPhaseRepository abstractPhaseRepository,
+                        TrainingPhaseService trainingPhaseService,
+                        TrainingDefinitionRepository trainingDefinitionRepository) {
         this.abstractPhaseRepository = abstractPhaseRepository;
         this.trainingPhaseService = trainingPhaseService;
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
     }
 
-    public void deletePhase(Long definitionId, Long phaseId) {
-        AbstractPhase phase = abstractPhaseRepository.findById(phaseId)
-                .orElseThrow(() -> new RuntimeException("Phase was not found"));
-        // TODO throw proper exception once kypo2-training is migrated
+    private TrainingDefinition findDefinitionById(Long id) {
+        return trainingDefinitionRepository.findById(id)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id)));
+    }
 
-        // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed)
+    public AbstractPhase findPhaseById(Long phaseId) {
+        return abstractPhaseRepository.findById(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
 
-        int phaseOrder = phase.getOrder();
-        abstractPhaseRepository.decreaseOrderAfterPhaseWasDeleted(definitionId, phaseOrder);
+    /**
+     * Deletes specific phase based on id
+     *
+     * @param definitionId - id of definition containing phase to be deleted
+     * @param phaseId      - id of phase to be deleted
+     * @throws EntityNotFoundException training definition or phase is not found.
+     * @throws EntityConflictException phase cannot be deleted in released or archived training definition.
+     */
+    public void deletePhase(Long definitionId, Long phaseId) {
+        TrainingDefinition trainingDefinition = findDefinitionById(definitionId);
+        if (!trainingDefinition.getState().equals(TDState.UNRELEASED))
+            throw new EntityConflictException(new EntityErrorDetail(AbstractPhase.class, "id", definitionId.getClass(), definitionId, TrainingDefinitionService.ARCHIVED_OR_RELEASED));
 
-        abstractPhaseRepository.delete(phase);
+        AbstractPhase phaseToDelete = findPhaseById(phaseId);
+        abstractPhaseRepository.delete(phaseToDelete);
+        int phaseOrder = phaseToDelete.getOrder();
+        abstractPhaseRepository.decreaseOrderAfterPhaseWasDeleted(definitionId, phaseOrder);
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
     }
 
     @Transactional(readOnly = true)
@@ -54,24 +87,39 @@ public class PhaseService {
         return BeanMapper.INSTANCE.toDtoList(phases);
     }
 
-    public void movePhaseToSpecifiedOrder(Long phaseIdFrom, int newPosition) {
+    /**
+     * Move phase to the different position and modify orders of phases between moved phase and new position.
+     *
+     * @param definitionId     - Id of definition containing phases, this training definition is updating its last edited column.
+     * @param phaseIdFrom - id of the phase to be moved to the new position
+     * @param newPosition      - position where phase will be moved
+     * @throws EntityNotFoundException training definition or one of the phases is not found.
+     * @throws EntityConflictException released or archived training definition cannot be modified.
+     */
+    public void movePhaseToSpecifiedOrder(Long definitionId, Long phaseIdFrom, int newPosition) {
+        Integer maxOrderOfLevel = abstractPhaseRepository.getCurrentMaxOrder(definitionId);
+        if (newPosition < 0) {
+            newPosition = 0;
+        } else if (newPosition > maxOrderOfLevel) {
+            newPosition = maxOrderOfLevel;
+        }
         AbstractPhase phaseFrom = abstractPhaseRepository.findById(phaseIdFrom)
-                .orElseThrow(() -> new RuntimeException("Phase was not found"));
-        // TODO throw proper exception once kypo2-training is migrated
-
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseIdFrom.getClass(), phaseIdFrom, PHASE_NOT_FOUND)));
         int fromOrder = phaseFrom.getOrder();
-        if (fromOrder < newPosition) {
-            abstractPhaseRepository.decreaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinitionId(), fromOrder, newPosition);
+        if (fromOrder == newPosition) {
+            return;
         } else if (fromOrder > newPosition) {
-            abstractPhaseRepository.increaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinitionId(), newPosition, fromOrder);
+            abstractPhaseRepository.increaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinition().getId(), newPosition, fromOrder);
         } else {
-            // nothing should be changed, no further actions needed
-            return;
+            abstractPhaseRepository.decreaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinition().getId(), fromOrder, newPosition);
         }
-
         phaseFrom.setOrder(newPosition);
         abstractPhaseRepository.save(phaseFrom);
-        trainingPhaseService.alignDecisionMatrixForPhasesInTrainingDefinition(phaseFrom.getTrainingDefinitionId());
+        trainingPhaseService.alignDecisionMatrixForPhasesInTrainingDefinition(phaseFrom.getTrainingDefinition().getId());
+    }
+
+    private LocalDateTime getCurrentTimeInUTC() {
+        return LocalDateTime.now(Clock.systemUTC());
     }
 
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java
index c2898101e8627ada78c7b5347aa406aec34fb493..b178de42d9367b0bb43bc043e446de59e946f96a 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java
@@ -1,37 +1,55 @@
 package cz.muni.ics.kypo.training.adaptive.service;
 
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.Question;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.QuestionPhaseRelation;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
 import cz.muni.ics.kypo.training.adaptive.dto.PhaseCreateDTO;
+import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.QuestionnairePhaseImportDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionPhaseRelationDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionnairePhaseDTO;
 import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionnaireUpdateDTO;
 import cz.muni.ics.kypo.training.adaptive.enums.PhaseTypeCreate;
 import cz.muni.ics.kypo.training.adaptive.enums.QuestionnaireType;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
 import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionPhaseRelationRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import java.time.Clock;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
+import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND;
+import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED;
+
 @Service
 public class QuestionnairePhaseService {
 
     private static final Logger LOG = LoggerFactory.getLogger(QuestionnairePhaseService.class);
 
+    private final TrainingDefinitionRepository trainingDefinitionRepository;
+    private final TrainingInstanceRepository trainingInstanceRepository;
     private final QuestionnairePhaseRepository questionnairePhaseRepository;
     private final AbstractPhaseRepository abstractPhaseRepository;
     private final QuestionRepository questionRepository;
@@ -39,9 +57,15 @@ public class QuestionnairePhaseService {
     private final QuestionPhaseRelationRepository questionPhaseRelationRepository;
 
     @Autowired
-    public QuestionnairePhaseService(QuestionnairePhaseRepository questionnairePhaseRepository, AbstractPhaseRepository abstractPhaseRepository,
-                                     QuestionRepository questionRepository, TrainingPhaseRepository trainingPhaseRepository,
+    public QuestionnairePhaseService(TrainingDefinitionRepository trainingDefinitionRepository,
+                                     TrainingInstanceRepository trainingInstanceRepository,
+                                     QuestionnairePhaseRepository questionnairePhaseRepository,
+                                     AbstractPhaseRepository abstractPhaseRepository,
+                                     QuestionRepository questionRepository,
+                                     TrainingPhaseRepository trainingPhaseRepository,
                                      QuestionPhaseRelationRepository questionPhaseRelationRepository) {
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
         this.questionnairePhaseRepository = questionnairePhaseRepository;
         this.abstractPhaseRepository = abstractPhaseRepository;
         this.questionRepository = questionRepository;
@@ -50,9 +74,12 @@ public class QuestionnairePhaseService {
     }
 
     public QuestionnairePhaseDTO createDefaultQuestionnairePhase(Long trainingDefinitionId, PhaseCreateDTO phaseCreateDTO) {
+        TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+
         QuestionnairePhase questionnairePhase = new QuestionnairePhase();
         questionnairePhase.setTitle("Title of questionnaire phase");
-        questionnairePhase.setTrainingDefinitionId(trainingDefinitionId);
+        questionnairePhase.setTrainingDefinition(trainingDefinition);
         questionnairePhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1);
 
         if (PhaseTypeCreate.QUESTIONNAIRE_ADAPTIVE.equals(phaseCreateDTO.getPhaseType())) {
@@ -62,34 +89,35 @@ public class QuestionnairePhaseService {
         }
 
         QuestionnairePhase persistedEntity = questionnairePhaseRepository.save(questionnairePhase);
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
         return BeanMapper.INSTANCE.toDto(persistedEntity);
     }
 
-    public QuestionnairePhaseDTO updateQuestionnairePhase(Long definitionId, Long phaseId, QuestionnaireUpdateDTO questionnaireUpdateDto) {
-        QuestionnairePhase questionnairePhase = BeanMapper.INSTANCE.toEntity(questionnaireUpdateDto);
-        questionnairePhase.setId(phaseId);
-
-        QuestionnairePhase persistedQuestionnairePhase = questionnairePhaseRepository.findById(questionnairePhase.getId())
-                .orElseThrow(() -> new RuntimeException("Questionnaire phase was not found"));
-        // TODO throw proper exception once kypo2-training is migrated
-
-        // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed);
-
-        questionnairePhase.setTrainingDefinitionId(persistedQuestionnairePhase.getTrainingDefinitionId());
-        questionnairePhase.setOrder(persistedQuestionnairePhase.getOrder());
-        questionnairePhase.setQuestionnaireType(persistedQuestionnairePhase.getQuestionnaireType());
-
-        questionnairePhase.getQuestionPhaseRelations().clear();
-        questionnairePhase.getQuestionPhaseRelations().addAll(resolveQuestionnairePhaseRelationsUpdate(questionnairePhase, questionnaireUpdateDto));
-
-        if (!CollectionUtils.isEmpty(questionnairePhase.getQuestions())) {
-            questionnairePhase.getQuestions().forEach(x -> x.setQuestionnairePhase(questionnairePhase));
-            for (Question question : questionnairePhase.getQuestions()) {
+    public QuestionnairePhaseDTO updateQuestionnairePhase(Long definitionId, Long phaseId, QuestionnaireUpdateDTO questionnaireUpdate) {
+        QuestionnairePhase questionnairePhaseToUpdate = BeanMapper.INSTANCE.toEntity(questionnaireUpdate);
+        TrainingDefinition trainingDefinition = findDefinitionById(definitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+        if (!existPhaseInDefinition(trainingDefinition, questionnairePhaseToUpdate.getId())) {
+            throw new EntityNotFoundException(new EntityErrorDetail(QuestionnairePhase.class, "id", questionnairePhaseToUpdate.getId().getClass(),
+                    questionnairePhaseToUpdate.getId(), "Phase was not found in definition (id: " + definitionId + ")."));
+        }
+        QuestionnairePhase persistedQuestionnairePhase = findQuestionnairePhaseById(phaseId);
+        questionnairePhaseToUpdate.setId(phaseId);
+        questionnairePhaseToUpdate.setTrainingDefinition(persistedQuestionnairePhase.getTrainingDefinition());
+        questionnairePhaseToUpdate.setOrder(persistedQuestionnairePhase.getOrder());
+        questionnairePhaseToUpdate.setQuestionnaireType(persistedQuestionnairePhase.getQuestionnaireType());
+
+        questionnairePhaseToUpdate.getQuestionPhaseRelations().clear();
+        questionnairePhaseToUpdate.getQuestionPhaseRelations().addAll(resolveQuestionnairePhaseRelationsUpdate(questionnairePhaseToUpdate, questionnaireUpdate));
+
+        if (!CollectionUtils.isEmpty(questionnairePhaseToUpdate.getQuestions())) {
+            questionnairePhaseToUpdate.getQuestions().forEach(x -> x.setQuestionnairePhase(questionnairePhaseToUpdate));
+            for (Question question : questionnairePhaseToUpdate.getQuestions()) {
                 question.getChoices().forEach(x -> x.setQuestion(question));
             }
         }
 
-        QuestionnairePhase savedEntity = questionnairePhaseRepository.save(questionnairePhase);
+        QuestionnairePhase savedEntity = questionnairePhaseRepository.save(questionnairePhaseToUpdate);
         return BeanMapper.INSTANCE.toDto(savedEntity);
     }
 
@@ -131,4 +159,35 @@ public class QuestionnairePhaseService {
         return questionnairePhaseRelations;
     }
 
+    private QuestionnairePhase findQuestionnairePhaseById(Long phaseId) {
+        return questionnairePhaseRepository.findById(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(QuestionnairePhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
+
+    private TrainingDefinition findDefinitionById(Long id) {
+        return trainingDefinitionRepository.findById(id)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id)));
+    }
+
+    private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) {
+        if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    ARCHIVED_OR_RELEASED));
+        }
+        if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    "Cannot update training definition with already created training instance. " +
+                            "Remove training instance/s before updating training definition."));
+        }
+    }
+
+    private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) {
+        return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId)
+                .isPresent();
+    }
+
+    private LocalDateTime getCurrentTimeInUTC() {
+        return LocalDateTime.now(Clock.systemUTC());
+    }
+
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java
index c30730dd93ad5eb111934a069d16866a527964f2..30ef420e0797214d26dfc79ddc42d57afb2d5197 100644
--- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java
@@ -1,12 +1,19 @@
 package cz.muni.ics.kypo.training.adaptive.service;
 
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.DecisionMatrixRow;
 import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
 import cz.muni.ics.kypo.training.adaptive.dto.training.TrainingPhaseDTO;
-import cz.muni.ics.kypo.training.adaptive.dto.training.TrainingPhaseUpdateDTO;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
 import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
 import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -14,12 +21,17 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
+import java.time.Clock;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
+import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND;
+import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED;
+
 @Service
 @Transactional
 public class TrainingPhaseService {
@@ -27,48 +39,73 @@ public class TrainingPhaseService {
     private static final Logger LOG = LoggerFactory.getLogger(TrainingPhaseService.class);
 
     private final TrainingPhaseRepository trainingPhaseRepository;
+    private final TrainingInstanceRepository trainingInstanceRepository;
     private final AbstractPhaseRepository abstractPhaseRepository;
+    private final TrainingDefinitionRepository trainingDefinitionRepository;
 
     @Autowired
-    public TrainingPhaseService(TrainingPhaseRepository trainingPhaseRepository, AbstractPhaseRepository abstractPhaseRepository) {
+    public TrainingPhaseService(TrainingDefinitionRepository trainingDefinitionRepository,
+                                TrainingInstanceRepository trainingInstanceRepository,
+                                TrainingPhaseRepository trainingPhaseRepository,
+                                AbstractPhaseRepository abstractPhaseRepository) {
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
         this.trainingPhaseRepository = trainingPhaseRepository;
         this.abstractPhaseRepository = abstractPhaseRepository;
     }
 
     public TrainingPhaseDTO createDefaultTrainingPhase(Long trainingDefinitionId) {
+        TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+
         TrainingPhase trainingPhase = new TrainingPhase();
         trainingPhase.setTitle("Title of training phase");
-        trainingPhase.setTrainingDefinitionId(trainingDefinitionId);
+        trainingPhase.setTrainingDefinition(trainingDefinition);
         trainingPhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1);
 
         trainingPhase.setDecisionMatrix(prepareDefaultDecisionMatrix(trainingDefinitionId, trainingPhase));
 
         TrainingPhase persistedEntity = trainingPhaseRepository.save(trainingPhase);
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
         return BeanMapper.INSTANCE.toDto(persistedEntity);
     }
 
-    public TrainingPhaseDTO updateTrainingPhase(Long definitionId, Long phaseId, TrainingPhaseUpdateDTO trainingPhaseUpdate) {
-        TrainingPhase trainingPhase = BeanMapper.INSTANCE.toEntity(trainingPhaseUpdate);
-        trainingPhase.setId(phaseId);
-
-        TrainingPhase persistedTrainingPhase = trainingPhaseRepository.findById(trainingPhase.getId())
-                .orElseThrow(() -> new RuntimeException("Training phase was not found"));
-        // TODO throw proper exception once kypo2-training is migrated
 
-        // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed);
-
-        trainingPhase.setTrainingDefinitionId(persistedTrainingPhase.getTrainingDefinitionId());
-        trainingPhase.setOrder(persistedTrainingPhase.getOrder());
-        trainingPhase.setTasks(persistedTrainingPhase.getTasks());
+    /**
+     * Updates training phase in training definition
+     *
+     * @param definitionId      - id of training definition containing phase to be updated
+     * @param trainingPhaseToUpdate phase to be updated
+     * @throws EntityNotFoundException training definition is not found.
+     * @throws EntityConflictException phase cannot be updated in released or archived training definition.
+     */
+    public TrainingPhaseDTO updateTrainingPhase(Long definitionId, Long phaseId, TrainingPhase trainingPhaseToUpdate) {
+        TrainingDefinition trainingDefinition = findDefinitionById(definitionId);
+        checkIfCanBeUpdated(trainingDefinition);
+        if (!existPhaseInDefinition(trainingDefinition, trainingPhaseToUpdate.getId())) {
+            throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", trainingPhaseToUpdate.getId().getClass(),
+                    trainingPhaseToUpdate.getId(), "Level was not found in definition (id: " + definitionId + ")."));
+        }
 
-        if (!CollectionUtils.isEmpty(trainingPhase.getDecisionMatrix())) {
-            trainingPhase.getDecisionMatrix().forEach(x -> x.setTrainingPhase(trainingPhase));
+        TrainingPhase persistedTrainingPhase = findPhaseById(phaseId);
+        trainingPhaseToUpdate.setId(phaseId);
+        trainingPhaseToUpdate.setTrainingDefinition(persistedTrainingPhase.getTrainingDefinition());
+        trainingPhaseToUpdate.setOrder(persistedTrainingPhase.getOrder());
+        trainingPhaseToUpdate.setTasks(persistedTrainingPhase.getTasks());
+        //TODO check that matrices are part of the persisted training phase
+        if (!CollectionUtils.isEmpty(trainingPhaseToUpdate.getDecisionMatrix())) {
+            trainingPhaseToUpdate.getDecisionMatrix().forEach(x -> x.setTrainingPhase(trainingPhaseToUpdate));
         }
 
-        TrainingPhase savedEntity = trainingPhaseRepository.save(trainingPhase);
+        TrainingPhase savedEntity = trainingPhaseRepository.save(trainingPhaseToUpdate);
         return BeanMapper.INSTANCE.toDto(savedEntity);
     }
 
+    public TrainingPhase findPhaseById(Long phaseId) {
+        return trainingPhaseRepository.findById(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
+
     public void alignDecisionMatrixForPhasesInTrainingDefinition(Long trainingDefinitionId) {
         List<TrainingPhase> trainingPhases = trainingPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingDefinitionId);
 
@@ -140,4 +177,30 @@ public class TrainingPhaseService {
                 .collect(Collectors.toList());
 
     }
+
+    private TrainingDefinition findDefinitionById(Long id) {
+        return trainingDefinitionRepository.findById(id)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id)));
+    }
+
+    private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) {
+        if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    ARCHIVED_OR_RELEASED));
+        }
+        if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    "Cannot update training definition with already created training instance. " +
+                            "Remove training instance/s before updating training definition."));
+        }
+    }
+
+    private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) {
+        return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId)
+                .isPresent();
+    }
+
+    private LocalDateTime getCurrentTimeInUTC() {
+        return LocalDateTime.now(Clock.systemUTC());
+    }
 }
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..c18311a25c770c6f3bf5259170b0282103440468
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java
@@ -0,0 +1,137 @@
+package cz.muni.ics.kypo.training.adaptive.service.api;
+
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The type User service.
+ */
+@Service
+public class ElasticsearchServiceApi {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchServiceApi.class);
+    private WebClient elasticsearchServiceWebClient;
+
+    /**
+     * Instantiates a new ElasticSearchService API.
+     *
+     * @param elasticsearchServiceWebClient the web client
+     */
+    public ElasticsearchServiceApi(@Qualifier("elasticsearchServiceWebClient") WebClient elasticsearchServiceWebClient) {
+        this.elasticsearchServiceWebClient = elasticsearchServiceWebClient;
+    }
+
+    /**
+     * Deletes events from elasticsearch for particular training instance
+     *
+     * @param trainingInstanceId id of the training instance whose events to delete.
+     * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice.
+     */
+    public void deleteEventsByTrainingInstanceId(Long trainingInstanceId){
+        try {
+            elasticsearchServiceWebClient
+                    .delete()
+                    .uri("/training-platform-events/training-instances/{instanceId}", trainingInstanceId)
+                    .retrieve()
+                    .bodyToMono(Void.class)
+                    .block();
+        } catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API to delete events for particular instance (ID: "+ trainingInstanceId +")", ex.getApiSubError());
+        }
+    }
+
+
+    /**
+     * Obtain events from elasticsearch for particular training run
+     *
+     * @param trainingRun thee training run whose events to obtain.
+     * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice.
+     */
+    public List<Map<String, Object>> findAllEventsFromTrainingRun(TrainingRun trainingRun){
+        try {
+            Long definitionId = trainingRun.getTrainingInstance().getTrainingDefinition().getId();
+            Long instanceId = trainingRun.getTrainingInstance().getId();
+            return elasticsearchServiceWebClient
+                    .get()
+                    .uri("/training-platform-events/training-definitions/{definitionId}/training-instances/{instanceId}/training-runs/{runId}", definitionId, instanceId, trainingRun.getId())
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
+                    .block();
+        } catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API for particular run (ID: "+ trainingRun.getId() +")", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Deletes events from elasticsearch for particular training run
+     *
+     * @param trainingInstanceId id of the training instance in which the training run is running.
+     * @param trainingRunId id of the training run whose events to delete.
+     * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice.
+     */
+    public void deleteEventsFromTrainingRun(Long trainingInstanceId, Long trainingRunId){
+        try {
+            elasticsearchServiceWebClient
+                    .delete()
+                    .uri("/training-platform-events/training-instances/{instanceId}/training-runs/{runId}", trainingInstanceId, trainingRunId)
+                    .retrieve()
+                    .bodyToMono(Void.class)
+                    .block();
+        } catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API to delete events for particular training run (ID: "+ trainingRunId +")", ex.getApiSubError());
+        }
+    }
+
+    public List<Map<String, Object>> findAllConsoleCommandsFromSandbox(Long sandboxId){
+        try {
+            return elasticsearchServiceWebClient
+                    .get()
+                    .uri("/training-platform-commands/sandboxes/{sandboxId}", sandboxId)
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
+                    .block();
+        }catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API for particular sandbox (ID: "+ sandboxId +")", ex.getApiSubError());
+        }
+    }
+
+    public List<Map<String, Object>> findAllConsoleCommandsFromSandboxAndTimeRange(Integer sandboxId, Long from, Long to){
+        try {
+            return elasticsearchServiceWebClient
+                    .get()
+                    .uri(uriBuilder -> uriBuilder.path("/training-platform-commands/sandboxes/{sandboxId}/ranges")
+                            .queryParam("from", from)
+                            .queryParam("to", to)
+                            .build(sandboxId)
+                    )
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
+                    .block();
+        }catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API for particular commands of sandbox (ID: "+ sandboxId +")", ex.getApiSubError());
+        }
+    }
+
+    public void deleteBashCommandsFromPool(Long poolId){
+        try{
+            elasticsearchServiceWebClient
+                    .delete()
+                    .uri("/training-platform-commands/pools/{poolId}", poolId)
+                    .retrieve()
+                    .bodyToMono(Void.class)
+                    .block();
+        }catch (CustomWebClientException ex){
+            throw new MicroserviceApiException("Error when calling Elasticsearch API to delete bash commands for particular pool (ID: "+ poolId +")", ex.getApiSubError());
+        }
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6f90baeac405412af2bf2d98bce2d5fb20814ea
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java
@@ -0,0 +1,128 @@
+package cz.muni.ics.kypo.training.adaptive.service.api;
+
+import cz.muni.ics.kypo.training.adaptive.dto.responses.LockedPoolInfo;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.PoolInfoDTO;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxDefinitionInfo;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxInfo;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.ForbiddenException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.errors.PythonApiError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+
+/**
+ * The type User service.
+ */
+@Service
+public class SandboxServiceApi {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SandboxServiceApi.class);
+    private WebClient sandboxServiceWebClient;
+
+
+    /**
+     * Instantiates a new SandboxService API.
+     *
+     * @param sandboxServiceWebClient the web client
+     */
+    public SandboxServiceApi(@Qualifier("sandboxServiceWebClient") WebClient sandboxServiceWebClient) {
+        this.sandboxServiceWebClient = sandboxServiceWebClient;
+    }
+
+
+    public Long getAndLockSandboxForTrainingRun(Long poolId) {
+        try {
+            SandboxInfo sandboxInfo = sandboxServiceWebClient
+                    .get()
+                    .uri("/pools/{poolId}/sandboxes/get-and-lock", poolId)
+                    .retrieve()
+                    .bodyToMono(SandboxInfo.class)
+                    .block();
+            return sandboxInfo.getId();
+        } catch (CustomWebClientException ex) {
+            if (ex.getStatusCode() == HttpStatus.CONFLICT) {
+                throw new ForbiddenException("There is no available sandbox, wait a minute and try again or ask organizer to allocate more sandboxes.");
+            }
+            throw new MicroserviceApiException("Error when calling OpenStack Sandbox Service API to get unlocked sandbox from pool (ID: " + poolId + ")", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Lock pool locked pool info.
+     *
+     * @param poolId the pool id
+     * @return the locked pool info
+     */
+    public LockedPoolInfo lockPool(Long poolId) {
+        try {
+            return sandboxServiceWebClient
+                    .post()
+                    .uri("/pools/{poolId}/locks", poolId)
+                    .body(Mono.just("{}"), String.class)
+                    .retrieve()
+                    .bodyToMono(LockedPoolInfo.class)
+                    .block();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Currently, it is not possible to lock and assign pool with (ID: " + poolId + ").", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Unlock pool.
+     *
+     * @param poolId the pool id
+     */
+    public void unlockPool(Long poolId) {
+        try {
+            // get lock id from pool
+            PoolInfoDTO poolInfoDto = sandboxServiceWebClient
+                    .get()
+                    .uri("/pools/{poolId}", poolId)
+                    .retrieve()
+                    .bodyToMono(PoolInfoDTO.class)
+                    .block();
+            // unlock pool
+            if (poolInfoDto != null && poolInfoDto.getLockId() != null) {
+                sandboxServiceWebClient
+                        .delete()
+                        .uri("/pools/{poolId}/locks/{lockId}", poolId, poolInfoDto.getLockId())
+                        .retrieve()
+                        .bodyToMono(Void.class)
+                        .block();
+            }
+        } catch (CustomWebClientException ex) {
+            if(ex.getStatusCode() != HttpStatus.NOT_FOUND){
+                throw new MicroserviceApiException("Currently, it is not possible to unlock a pool with (ID: " + poolId + ").", ex.getApiSubError());
+            }
+        }
+    }
+
+    /**
+     * Gets sandbox definition id.
+     *
+     * @param poolId the pool id
+     * @return the sandbox definition id
+     */
+    public SandboxDefinitionInfo getSandboxDefinitionId(Long poolId) {
+        try {
+            return sandboxServiceWebClient
+                    .get()
+                    .uri("/pools/{poolId}/definition", poolId)
+                    .retrieve()
+                    .bodyToMono(SandboxDefinitionInfo.class)
+                    .block();
+        } catch (CustomWebClientException ex) {
+            if (ex.getStatusCode() == HttpStatus.CONFLICT) {
+                throw new ForbiddenException("There is no available sandbox definition for particular pool (ID: " + poolId + ").");
+            }
+            throw new MicroserviceApiException("Error when calling Python API to sandbox for particular pool (ID: " + poolId + ")", new PythonApiError(ex.getMessage()));
+        }
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d86e261dc8a029b26c417d93b6632d950970e96
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java
@@ -0,0 +1,202 @@
+package cz.muni.ics.kypo.training.adaptive.service.audit;
+
+import cz.muni.csirt.kypo.events.adaptive.trainings.*;
+import cz.muni.csirt.kypo.events.adaptive.trainings.enums.PhaseType;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * The type Audit events service.
+ */
+@Service
+public class AuditEventsService {
+
+    private AuditService auditService;
+
+    /**
+     * Instantiates a new Audit events service.
+     *
+     * @param auditService the audit service
+     */
+    @Autowired
+    public AuditEventsService(AuditService auditService) {
+        this.auditService = auditService;
+    }
+
+
+    /**
+     * Audit training run started action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditTrainingRunStartedAction(TrainingRun trainingRun) {
+        TrainingRunStarted.TrainingRunStartedBuilder<?, ?> trainingRunStartedBuilder = (TrainingRunStarted.TrainingRunStartedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, TrainingRunStarted.builder());
+
+        TrainingRunStarted trainingRunStarted = trainingRunStartedBuilder
+                .gameTime(0L)
+                .build();
+        auditService.saveTrainingRunEvent(trainingRunStarted, 0L);
+    }
+
+    /**
+     * Audit phase started action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditPhaseStartedAction(TrainingRun trainingRun) {
+        PhaseStarted.PhaseStartedBuilder<?, ?> phaseStartedBuilder = (PhaseStarted.PhaseStartedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, PhaseStarted.builder());
+
+        PhaseStarted phaseStarted = phaseStartedBuilder
+                .phaseType(getPhaseType(trainingRun.getCurrentPhase()))
+                .phaseTitle(trainingRun.getCurrentPhase().getTitle())
+                .build();
+        auditService.saveTrainingRunEvent(phaseStarted, 10L);
+    }
+
+    /**
+     * Audit phase completed action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditPhaseCompletedAction(TrainingRun trainingRun) {
+        PhaseCompleted.PhaseCompletedBuilder<?, ?> phaseCompletedBuilder = (PhaseCompleted.PhaseCompletedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, PhaseCompleted.builder());
+
+        PhaseCompleted phaseCompleted = phaseCompletedBuilder
+                .phaseType(getPhaseType(trainingRun.getCurrentPhase()))
+                .build();
+        auditService.saveTrainingRunEvent(phaseCompleted, 5L);
+    }
+
+    /**
+     * Audit solution displayed action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditSolutionDisplayedAction(TrainingRun trainingRun) {
+        SolutionDisplayed.SolutionDisplayedBuilder<?, ?> solutionDisplayedBuilder = (SolutionDisplayed.SolutionDisplayedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, SolutionDisplayed.builder());
+
+        SolutionDisplayed solutionDisplayed = solutionDisplayedBuilder
+                .build();
+        auditService.saveTrainingRunEvent(solutionDisplayed, 0L);
+    }
+
+    /**
+     * Audit correct flag submitted action.
+     *
+     * @param trainingRun the training run
+     * @param flag        the flag
+     */
+    public void auditCorrectFlagSubmittedAction(TrainingRun trainingRun, String flag) {
+        CorrectFlagSubmitted.CorrectFlagSubmittedBuilder<?, ?> correctFlagSubmittedBuilder = (CorrectFlagSubmitted.CorrectFlagSubmittedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, CorrectFlagSubmitted.builder());
+
+        CorrectFlagSubmitted correctFlagSubmitted = correctFlagSubmittedBuilder
+                .flagContent(flag)
+                .build();
+        auditService.saveTrainingRunEvent(correctFlagSubmitted, 0L);
+    }
+
+    /**
+     * Audit wrong flag submitted action.
+     *
+     * @param trainingRun the training run
+     * @param flag        the flag
+     */
+    public void auditWrongFlagSubmittedAction(TrainingRun trainingRun, String flag) {
+        WrongFlagSubmitted.WrongFlagSubmittedBuilder<?, ?> wrongFlagSubmittedBuilder = (WrongFlagSubmitted.WrongFlagSubmittedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, WrongFlagSubmitted.builder());
+
+        WrongFlagSubmitted wrongFlagSubmitted = wrongFlagSubmittedBuilder
+                .flagContent(flag)
+                .count(trainingRun.getIncorrectFlagCount())
+                .build();
+        auditService.saveTrainingRunEvent(wrongFlagSubmitted, 0L);
+    }
+
+    /**
+     * Audit assessment answers action.
+     *
+     * @param trainingRun the training run
+     * @param answers     the answers
+     */
+    public void auditAssessmentAnswersAction(TrainingRun trainingRun, String answers) {
+        QuestionnaireAnswers.QuestionnaireAnswersBuilder<?, ?> questionnaireAnswersBuilder = (QuestionnaireAnswers.QuestionnaireAnswersBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, QuestionnaireAnswers.builder());
+
+        QuestionnaireAnswers questionnaireAnswers = questionnaireAnswersBuilder
+                .answers(answers)
+                .build();
+        auditService.saveTrainingRunEvent(questionnaireAnswers, 0L);
+    }
+
+    /**
+     * Audit training run ended action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditTrainingRunEndedAction(TrainingRun trainingRun) {
+        TrainingRunEnded.TrainingRunEndedBuilder<?, ?> trainingRunEndedBuilder = (TrainingRunEnded.TrainingRunEndedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, TrainingRunEnded.builder());
+
+        TrainingRunEnded trainingRunEnded = trainingRunEndedBuilder
+                .startTime(trainingRun.getStartTime().atOffset(ZoneOffset.UTC).toInstant().toEpochMilli())
+                .endTime(System.currentTimeMillis())
+                .build();
+        auditService.saveTrainingRunEvent(trainingRunEnded, 10L);
+    }
+
+    /**
+     * Audit training run resumed action.
+     *
+     * @param trainingRun the training run
+     */
+    public void auditTrainingRunResumedAction(TrainingRun trainingRun) {
+        TrainingRunResumed.TrainingRunResumedBuilder<?, ?> trainingRunResumedBuilder = (TrainingRunResumed.TrainingRunResumedBuilder<?, ?>)
+                fillInCommonBuilderFields(trainingRun, TrainingRunResumed.builder());
+        TrainingRunResumed trainingRunResumed = trainingRunResumedBuilder.build();
+        auditService.saveTrainingRunEvent(trainingRunResumed, 0L);
+    }
+
+    private AbstractAuditAdaptivePOJO.AbstractAuditAdaptivePOJOBuilder<?, ?> fillInCommonBuilderFields(TrainingRun trainingRun, AbstractAuditAdaptivePOJO.AbstractAuditAdaptivePOJOBuilder<?, ?> builder) {
+        TrainingInstance trainingInstance = trainingRun.getTrainingInstance();
+        builder.sandboxId(trainingRun.getSandboxInstanceRefId())
+               .poolId(trainingInstance.getPoolId())
+               .trainingRunId(trainingRun.getId())
+               .trainingInstanceId(trainingInstance.getId())
+               .trainingDefinitionId(trainingInstance.getTrainingDefinition().getId())
+               .gameTime(computeGameTime(trainingRun.getStartTime()))
+               .userRefId(trainingRun.getParticipantRef().getUserRefId())
+               .phaseId(trainingRun.getCurrentPhase().getId());
+        return builder;
+    }
+
+    private long computeGameTime(LocalDateTime gameStartedTime) {
+        return ChronoUnit.MILLIS.between(gameStartedTime, LocalDateTime.now(Clock.systemUTC()));
+    }
+
+    private PhaseType getPhaseType(AbstractPhase abstractPhase) {
+        if (abstractPhase instanceof TrainingPhase) {
+            return PhaseType.TRAINING;
+        } else if (abstractPhase instanceof InfoPhase) {
+            return PhaseType.INFO;
+        } else if (abstractPhase instanceof QuestionnairePhase) {
+            return PhaseType.QUESTIONNAIRE;
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java
new file mode 100644
index 0000000000000000000000000000000000000000..6497b80273cbb8cf04620d73b0e9acbd91cae24e
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java
@@ -0,0 +1,55 @@
+package cz.muni.ics.kypo.training.adaptive.service.audit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import cz.muni.csirt.kypo.events.AbstractAuditPOJO;
+import cz.muni.csirt.kypo.events.adaptive.trainings.AbstractAuditAdaptivePOJO;
+import cz.muni.ics.kypo.training.adaptive.exceptions.ElasticsearchTrainingServiceLayerException;
+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.stereotype.Service;
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+
+/**
+ * The type Audit service.
+ */
+@Service
+public class AuditService {
+
+    private static Logger logger = LoggerFactory.getLogger(AuditService.class);
+
+    private ObjectMapper objectMapper;
+
+    /**
+     * Instantiates a new Audit service.
+     *
+     * @param objectMapper the object mapper
+     */
+    @Autowired
+    public AuditService(@Qualifier("objMapperForElasticsearch") ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    /**
+     * Method for saving general class into Elasticsearch under specific index and type.
+     *
+     * @param <T>       the type parameter
+     * @param pojoClass class saving to Elasticsearch
+     * @throws ElasticsearchTrainingServiceLayerException the elasticsearch training service layer exception
+     */
+    public <T extends AbstractAuditAdaptivePOJO> void saveTrainingRunEvent(T pojoClass, long timestampDelay) throws ElasticsearchTrainingServiceLayerException {
+        Assert.notNull(pojoClass, "Null class could not be saved via audit method.");
+        try {
+            pojoClass.setTimestamp(System.currentTimeMillis() + timestampDelay);
+            pojoClass.setType(pojoClass.getClass().getName());
+
+            logger.info(objectMapper.writeValueAsString(pojoClass));
+        } catch (IOException ex) {
+            throw new ElasticsearchTrainingServiceLayerException(ex);
+        }
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f5e5a729d13965ff80a12c42024666a8df6576b
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java
@@ -0,0 +1,119 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import cz.muni.ics.kypo.training.adaptive.exceptions.*;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+/**
+ * The type Export import service.
+ */
+@Service
+public class ExportImportService {
+
+    private TrainingDefinitionRepository trainingDefinitionRepository;
+    private AbstractPhaseRepository abstractPhaseRepository;
+    private QuestionnairePhaseRepository questionnairePhaseRepository;
+    private InfoPhaseRepository infoPhaseRepository;
+    private TrainingPhaseRepository trainingPhaseRepository;
+    private TrainingInstanceRepository trainingInstanceRepository;
+    private TrainingRunRepository trainingRunRepository;
+
+    /**
+     * Instantiates a new Export import service.
+     *
+     * @param trainingDefinitionRepository the training definition repository
+     * @param abstractPhaseRepository      the abstract level repository
+     * @param questionnairePhaseRepository    the assessment level repository
+     * @param infoPhaseRepository          the info level repository
+     * @param trainingPhaseRepository          the game level repository
+     * @param trainingInstanceRepository   the training instance repository
+     * @param trainingRunRepository        the training run repository
+     */
+    @Autowired
+    public ExportImportService(TrainingDefinitionRepository trainingDefinitionRepository,
+                               AbstractPhaseRepository abstractPhaseRepository,
+                               QuestionnairePhaseRepository questionnairePhaseRepository,
+                               InfoPhaseRepository infoPhaseRepository,
+                               TrainingPhaseRepository trainingPhaseRepository,
+                               TrainingInstanceRepository trainingInstanceRepository,
+                               TrainingRunRepository trainingRunRepository)
+    {
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
+        this.abstractPhaseRepository = abstractPhaseRepository;
+        this.questionnairePhaseRepository = questionnairePhaseRepository;
+        this.trainingPhaseRepository = trainingPhaseRepository;
+        this.infoPhaseRepository = infoPhaseRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.trainingRunRepository = trainingRunRepository;
+    }
+
+    /**
+     * Finds training definition with given id.
+     *
+     * @param trainingDefinitionId the id of definition to be found.
+     * @return the {@link TrainingDefinition} with the given id.
+     * @throws EntityNotFoundException if training definition was not found.
+     */
+    public TrainingDefinition findById(Long trainingDefinitionId) {
+        return trainingDefinitionRepository.findById(trainingDefinitionId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinitionId.getClass(),
+                        trainingDefinitionId)));
+    }
+
+    /**
+     * Creates a phase and connects it with training definition.
+     *
+     * @param phase      the {@link AbstractPhase} to be created.
+     * @param definition the {@link TrainingDefinition} to associate phase with.
+     */
+    public void createLevel(AbstractPhase phase, TrainingDefinition definition) {
+        phase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(definition.getId()) + 1);
+        phase.setTrainingDefinition(definition);
+        if (phase instanceof QuestionnairePhase) {
+            questionnairePhaseRepository.save((QuestionnairePhase) phase);
+        } else if (phase instanceof InfoPhase) {
+            infoPhaseRepository.save((InfoPhase) phase);
+        } else {
+            trainingPhaseRepository.save((TrainingPhase) phase);
+        }
+    }
+
+    /**
+     * Finds training instance with given id.
+     *
+     * @param trainingInstanceId the id of instance to be found.
+     * @return the {@link TrainingInstance} with the given id.
+     * @throws EntityNotFoundException if training instance was not found.
+     */
+    public TrainingInstance findInstanceById(Long trainingInstanceId) {
+        return trainingInstanceRepository.findById(trainingInstanceId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstanceId.getClass(),
+                        trainingInstanceId)));
+    }
+
+    /**
+     * Finds training runs associated with training instance with given id.
+     *
+     * @param trainingInstanceId the id of instance which runs are to be found.
+     * @return the set off all {@link TrainingRun}
+     */
+    public Set<TrainingRun> findRunsByInstanceId(Long trainingInstanceId) {
+        return trainingRunRepository.findAllByTrainingInstanceId(trainingInstanceId);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java
new file mode 100644
index 0000000000000000000000000000000000000000..f149e48e0563e9fd45b48fe9f3427f486b531864
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java
@@ -0,0 +1,184 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalRO;
+import cz.muni.ics.kypo.training.adaptive.domain.UserRef;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import cz.muni.ics.kypo.training.adaptive.dto.UserRefDTO;
+import cz.muni.ics.kypo.training.adaptive.enums.RoleTypeSecurity;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.web.reactive.function.client.WebClient;
+
+
+/**
+ * The type Security service.
+ */
+@Service
+@TransactionalRO(propagation = Propagation.REQUIRES_NEW)
+public class SecurityService {
+
+    private TrainingRunRepository trainingRunRepository;
+    private TrainingDefinitionRepository trainingDefinitionRepository;
+    private TrainingInstanceRepository trainingInstanceRepository;
+    private WebClient userManagementWebClient;
+
+    /**
+     * Instantiates a new Security service.
+     *
+     * @param trainingInstanceRepository   the training instance repository
+     * @param trainingDefinitionRepository the training definition repository
+     * @param trainingRunRepository        the training run repository
+     * @param userManagementWebClient      the java rest template
+     */
+    @Autowired
+    public SecurityService(TrainingInstanceRepository trainingInstanceRepository,
+                           TrainingDefinitionRepository trainingDefinitionRepository,
+                           TrainingRunRepository trainingRunRepository,
+                           @Qualifier("userManagementServiceWebClient") WebClient userManagementWebClient) {
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.trainingRunRepository = trainingRunRepository;
+        this.userManagementWebClient = userManagementWebClient;
+    }
+
+    /**
+     * Is trainee of given training run boolean.
+     *
+     * @param trainingRunId the training run id
+     * @return the boolean
+     */
+    public boolean isTraineeOfGivenTrainingRun(Long trainingRunId) {
+        TrainingRun trainingRun = trainingRunRepository.findById(trainingRunId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(),
+                        trainingRunId, "The necessary permissions are required for a resource.")));
+        return trainingRun.getParticipantRef().getUserRefId().equals(getUserRefIdFromUserAndGroup());
+    }
+
+    /**
+     * Is organizer of given training instance boolean.
+     *
+     * @param instanceId the instance id
+     * @return the boolean
+     */
+    public boolean isOrganizerOfGivenTrainingInstance(Long instanceId) {
+        TrainingInstance trainingInstance = trainingInstanceRepository.findById(instanceId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(),
+                        instanceId, "The necessary permissions are required for a resource.")));
+        return trainingInstance.getOrganizers().stream()
+                .anyMatch(o -> o.getUserRefId().equals(getUserRefIdFromUserAndGroup()));
+    }
+
+    /**
+     * Is organizer of given training run.
+     *
+     * @param trainingRunId the run id
+     * @return the boolean
+     */
+    public boolean isOrganizerOfGivenTrainingRun(Long trainingRunId) {
+        TrainingRun trainingRun = trainingRunRepository.findById(trainingRunId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(),
+                        trainingRunId, "The necessary permissions are required for a resource.")));
+        return trainingRun.getTrainingInstance().getOrganizers().stream()
+                .anyMatch(o -> o.getUserRefId().equals(getUserRefIdFromUserAndGroup()));
+    }
+
+    /**
+     * Is designer of given training definition boolean.
+     *
+     * @param definitionId the definition id
+     * @return the boolean
+     */
+    public boolean isDesignerOfGivenTrainingDefinition(Long definitionId) {
+        TrainingDefinition trainingDefinition = trainingDefinitionRepository.findById(definitionId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class,
+                        "id", definitionId.getClass(), definitionId, "The necessary permissions are required for a resource.")));
+        return trainingDefinition.getAuthors().stream()
+                .anyMatch(a -> a.getUserRefId().equals(getUserRefIdFromUserAndGroup()));
+    }
+
+    /**
+     * Has role boolean.
+     *
+     * @param roleTypeSecurity the role type security
+     * @return the boolean
+     */
+    public boolean hasRole(RoleTypeSecurity roleTypeSecurity) {
+        OAuth2Authentication authentication = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
+        for (GrantedAuthority gA : authentication.getUserAuthentication().getAuthorities()) {
+            if (gA.getAuthority().equals(roleTypeSecurity.name())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets user ref id from user and group.
+     *
+     * @return the user ref id from user and group
+     */
+    public Long getUserRefIdFromUserAndGroup() {
+        try {
+            UserRefDTO userRefDTO = userManagementWebClient
+                    .get()
+                    .uri("/users/info")
+                    .retrieve()
+                    .bodyToMono(UserRefDTO.class)
+                    .block();
+            checkNonNull(userRefDTO, "Returned null response when calling user management service API to get info about logged in user.");
+            return userRefDTO.getUserRefId();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to get info about logged in user.", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Create user ref entity by info from user and group user ref.
+     *
+     * @return the user ref
+     */
+    public UserRef createUserRefEntityByInfoFromUserAndGroup() {
+        try {
+            UserRefDTO userRefDTO = userManagementWebClient
+                    .get()
+                    .uri("/users/info")
+                    .retrieve()
+                    .bodyToMono(UserRefDTO.class)
+                    .block();
+            checkNonNull(userRefDTO, "Returned null response when calling user management service API to get info about logged in user.");
+            UserRef userRef = new UserRef();
+            userRef.setUserRefId(userRefDTO.getUserRefId());
+            return userRef;
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to get info about logged in user.", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Check if response from external API is not null.
+     *
+     * @param object object to check
+     * @param message exception message if response is null
+     * @throws MicroserviceApiException if response is null
+     */
+    private void checkNonNull(Object object, String message) {
+        if (object == null) {
+            throw new MicroserviceApiException(message);
+        }
+    }
+
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4857ddaf6ef91aa7aaa3cd7df87242d5b69aa722
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java
@@ -0,0 +1,405 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.querydsl.core.types.Predicate;
+import cz.muni.ics.kypo.training.adaptive.domain.UserRef;
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.*;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.Question;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.QuestionPhaseRelation;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
+import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * The type Training definition service.
+ */
+@Service
+public class TrainingDefinitionService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TrainingDefinitionService.class);
+
+    private TrainingDefinitionRepository trainingDefinitionRepository;
+    private TrainingInstanceRepository trainingInstanceRepository;
+    private AbstractPhaseRepository abstractPhaseRepository;
+    private TrainingPhaseRepository trainingPhaseRepository;
+    private InfoPhaseRepository infoPhaseRepository;
+    private QuestionnairePhaseRepository questionnairePhaseRepository;
+    private UserRefRepository userRefRepository;
+    private SecurityService securityService;
+    private ObjectMapper objectMapper;
+
+    public static final String ARCHIVED_OR_RELEASED = "Cannot edit released or archived training definition.";
+    private static final String PHASE_NOT_FOUND = "Phase not found.";
+
+    /**
+     * Instantiates a new Training definition service.
+     *
+     * @param trainingDefinitionRepository the training definition repository
+     * @param abstractPhaseRepository      the abstract phase repository
+     * @param infoPhaseRepository          the info phase repository
+     * @param trainingPhaseRepository          the training phase repository
+     * @param questionnairePhaseRepository    the questionnaire phase repository
+     * @param trainingInstanceRepository   the training instance repository
+     * @param userRefRepository            the user ref repository
+     * @param securityService              the security service
+     */
+    @Autowired
+    public TrainingDefinitionService(TrainingDefinitionRepository trainingDefinitionRepository,
+                                     AbstractPhaseRepository abstractPhaseRepository,
+                                     InfoPhaseRepository infoPhaseRepository,
+                                     TrainingPhaseRepository trainingPhaseRepository,
+                                     QuestionnairePhaseRepository questionnairePhaseRepository,
+                                     TrainingInstanceRepository trainingInstanceRepository,
+                                     UserRefRepository userRefRepository,
+                                     SecurityService securityService,
+                                     ObjectMapper objectMapper) {
+        this.trainingDefinitionRepository = trainingDefinitionRepository;
+        this.abstractPhaseRepository = abstractPhaseRepository;
+        this.trainingPhaseRepository = trainingPhaseRepository;
+        this.infoPhaseRepository = infoPhaseRepository;
+        this.questionnairePhaseRepository = questionnairePhaseRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.userRefRepository = userRefRepository;
+        this.securityService = securityService;
+        this.objectMapper = objectMapper;
+    }
+
+    /**
+     * Finds specific Training Definition by id
+     *
+     * @param id of a Training Definition that would be returned
+     * @return specific {@link TrainingDefinition} by id
+     * @throws EntityNotFoundException training definition cannot be found
+     */
+    public TrainingDefinition findById(Long id) {
+        return trainingDefinitionRepository.findById(id)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id)));
+    }
+
+    /**
+     * Find all Training Definitions by author if user is designer or all Training Definitions if user is admin.
+     *
+     * @param predicate represents a predicate (boolean-valued function) of one argument.
+     * @param pageable  pageable parameter with information about pagination.
+     * @return all {@link TrainingDefinition}s
+     */
+    public Page<TrainingDefinition> findAll(Predicate predicate, Pageable pageable) {
+        return trainingDefinitionRepository.findAll(predicate, pageable);
+    }
+
+    /**
+     * Find all page.
+     *
+     * @param predicate      the predicate
+     * @param pageable       the pageable
+     * @param loggedInUserId the logged in user id
+     * @return the page
+     */
+    public Page<TrainingDefinition> findAll(Predicate predicate, Pageable pageable, Long loggedInUserId) {
+        return trainingDefinitionRepository.findAll(predicate, pageable, loggedInUserId);
+    }
+
+    /**
+     * Finds all Training Definitions accessible to users with the role of organizer.
+     *
+     * @param state    represents a state of training definition if it is released or unreleased.
+     * @param pageable pageable parameter with information about pagination.
+     * @return all Training Definitions for organizers
+     */
+    public Page<TrainingDefinition> findAllForOrganizers(TDState state, Pageable pageable) {
+        return trainingDefinitionRepository.findAllForOrganizers(state, pageable);
+    }
+
+    /**
+     * Find all for designers and organizers unreleased page.
+     *
+     * @param loggedInUserId the logged in user id
+     * @param pageable       the pageable
+     * @return the page
+     */
+    public Page<TrainingDefinition> findAllForDesigners(Long loggedInUserId, Pageable pageable) {
+        return trainingDefinitionRepository.findAllForDesigners(loggedInUserId, pageable);
+    }
+
+    /**
+     * creates new training definition
+     *
+     * @param trainingDefinition to be created
+     * @return new {@link TrainingDefinition}
+     */
+    public TrainingDefinition create(TrainingDefinition trainingDefinition) {
+        addLoggedInUserToTrainingDefinitionAsAuthor(trainingDefinition);
+        LOG.info("Training definition with id: {} created.", trainingDefinition.getId());
+        return trainingDefinitionRepository.save(trainingDefinition);
+    }
+
+    /**
+     * Updates given Training Definition
+     *
+     * @param trainingDefinitionToUpdate to be updated
+     * @throws EntityNotFoundException training definition or one of the phases is not found.
+     * @throws EntityConflictException released or archived training definition cannot be modified.
+     */
+    public void update(TrainingDefinition trainingDefinitionToUpdate) {
+        TrainingDefinition trainingDefinition = findById(trainingDefinitionToUpdate.getId());
+        checkIfCanBeUpdated(trainingDefinition);
+        addLoggedInUserToTrainingDefinitionAsAuthor(trainingDefinitionToUpdate);
+        trainingDefinitionToUpdate.setEstimatedDuration(trainingDefinition.getEstimatedDuration());
+        trainingDefinitionRepository.save(trainingDefinitionToUpdate);
+        LOG.info("Training definition with id: {} updated.", trainingDefinitionToUpdate.getId());
+    }
+
+    /**
+     * Creates new training definition by cloning existing one
+     *
+     * @param id    of definition to be cloned
+     * @param title the title of the new cloned definition
+     * @return cloned {@link TrainingDefinition}
+     * @throws EntityNotFoundException training definition not found.
+     * @throws EntityConflictException cannot clone unreleased training definition.
+     */
+    public TrainingDefinition clone(Long id, String title) {
+        TrainingDefinition trainingDefinition = findById(id);
+        TrainingDefinition clonedTrainingDefinition = objectMapper.convertValue(trainingDefinition, TrainingDefinition.class);
+        clonedTrainingDefinition.setId(null);
+
+        clonedTrainingDefinition.setTitle(title);
+        clonedTrainingDefinition.setState(TDState.UNRELEASED);
+        clonedTrainingDefinition.setAuthors(new HashSet<>());
+
+        addLoggedInUserToTrainingDefinitionAsAuthor(clonedTrainingDefinition);
+        clonedTrainingDefinition = trainingDefinitionRepository.save(clonedTrainingDefinition);
+        clonePhasesFromTrainingDefinition(trainingDefinition.getId(), clonedTrainingDefinition);
+
+        LOG.info("Training definition with id: {} cloned.", trainingDefinition.getId());
+        return clonedTrainingDefinition;
+    }
+
+    /**
+     * Deletes specific training definition based on id
+     *
+     * @param definitionId of definition to be deleted
+     * @throws EntityNotFoundException training definition or phase is not found.
+     * @throws EntityConflictException released training definition cannot be deleted.
+     */
+    public void delete(Long definitionId) {
+        TrainingDefinition definition = findById(definitionId);
+        if (definition.getState().equals(TDState.RELEASED))
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId,
+                    "Cannot delete released training definition."));
+        if (trainingInstanceRepository.existsAnyForTrainingDefinition(definitionId)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId,
+                    "Cannot delete training definition with already created training instance. " +
+                            "Remove training instance/s before deleting training definition."));
+        }
+        List<AbstractPhase> abstractPhases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId);
+        abstractPhases.forEach(this::deletePhase);
+        trainingDefinitionRepository.delete(definition);
+    }
+
+
+    /**
+     * Finds all phases from single definition
+     *
+     * @param definitionId of definition
+     * @return list of {@link AbstractPhase} associated with training definition
+     */
+    public List<AbstractPhase> findAllPhasesFromDefinition(Long definitionId) {
+        return abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId);
+    }
+
+    /**
+     * Finds specific phase by id with associated training definition
+     *
+     * @param phaseId - id of wanted phase
+     * @return wanted {@link AbstractPhase}
+     * @throws EntityNotFoundException phase is not found.
+     */
+    public AbstractPhase findPhaseByIdWithDefinition(Long phaseId) {
+        return abstractPhaseRepository.findByIdWithDefinition(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
+
+    /**
+     * Finds specific phase by id
+     *
+     * @param phaseId - id of wanted phase
+     * @return wanted {@link AbstractPhase}
+     * @throws EntityNotFoundException phase is not found.
+     */
+    private AbstractPhase findPhaseById(Long phaseId) {
+        return abstractPhaseRepository.findById(phaseId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND)));
+    }
+
+    /**
+     * Find all training instances associated with training definition by id.
+     *
+     * @param id the id of training definition
+     * @return the list of all {@link TrainingInstance}s associated with wanted {@link TrainingDefinition}
+     */
+    public List<TrainingInstance> findAllTrainingInstancesByTrainingDefinitionId(Long id) {
+        return trainingInstanceRepository.findAllByTrainingDefinitionId(id);
+    }
+
+    /**
+     * Switch development state of definition from unreleased to released, or from released to archived or back to unreleased.
+     *
+     * @param definitionId - id of training definition
+     * @param state        - new state of training definition
+     */
+    public void switchState(Long definitionId, TDState state) {
+        TrainingDefinition trainingDefinition = findById(definitionId);
+        if (trainingDefinition.getState().name().equals(state.name())) {
+            return;
+        }
+        switch (trainingDefinition.getState()) {
+            case UNRELEASED:
+                if (state.equals(TDState.RELEASED))
+                    trainingDefinition.setState(TDState.RELEASED);
+                else
+                    throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId,
+                            "Cannot switch from" + trainingDefinition.getState() + " to " + state));
+                break;
+            case RELEASED:
+                if (state.equals(TDState.ARCHIVED))
+                    trainingDefinition.setState(TDState.ARCHIVED);
+                else if (state.equals(TDState.UNRELEASED)) {
+                    if (trainingInstanceRepository.existsAnyForTrainingDefinition(definitionId)) {
+                        throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId,
+                                "Cannot update training definition with already created training instance(s). " +
+                                        "Remove training instance(s) before changing the state from released to unreleased training definition."));
+                    }
+                    trainingDefinition.setState((TDState.UNRELEASED));
+                }
+                break;
+            default:
+                throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId,
+                        "Cannot switch from " + trainingDefinition.getState() + " to " + state));
+        }
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
+    }
+
+    private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long phaseId) {
+        return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), phaseId)
+                .isPresent();
+    }
+
+    private void clonePhasesFromTrainingDefinition(Long trainingDefinitionId, TrainingDefinition clonedTrainingDefinition) {
+        List<AbstractPhase> phases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingDefinitionId);
+        if (phases == null || phases.isEmpty()) {
+            return;
+        }
+        phases.forEach(phase -> {
+            if (phase instanceof QuestionnairePhase) {
+                cloneQuestionnairePhase((QuestionnairePhase) phase, clonedTrainingDefinition);
+            }
+            if (phase instanceof InfoPhase) {
+                cloneInfoPhase((InfoPhase) phase, clonedTrainingDefinition);
+            }
+            if (phase instanceof TrainingPhase) {
+                cloneTrainingPhase((TrainingPhase) phase, clonedTrainingDefinition);
+            }
+        });
+    }
+
+    private void cloneInfoPhase(InfoPhase infoPhase, TrainingDefinition trainingDefinition) {
+        InfoPhase clonedInfoPhase = objectMapper.convertValue(infoPhase, InfoPhase.class);
+        clonedInfoPhase.setId(null);
+        clonedInfoPhase.setTrainingDefinition(trainingDefinition);
+        infoPhaseRepository.save(clonedInfoPhase);
+    }
+
+    private void cloneQuestionnairePhase(QuestionnairePhase questionnairePhase, TrainingDefinition trainingDefinition) {
+        QuestionnairePhase clonedQuestionnairePhase = objectMapper.convertValue(questionnairePhase, QuestionnairePhase.class);
+        for (Question question: clonedQuestionnairePhase.getQuestions()) {
+            question.setQuestionnairePhase(clonedQuestionnairePhase);
+        }
+        QuestionPhaseRelation questionPhaseRelation = new QuestionPhaseRelation();
+        clonedQuestionnairePhase.setId(null);
+        clonedQuestionnairePhase.setTrainingDefinition(trainingDefinition);
+        questionnairePhaseRepository.save(clonedQuestionnairePhase);
+    }
+
+    private void cloneTrainingPhase(TrainingPhase trainingPhase, TrainingDefinition trainingDefinition) {
+        TrainingPhase clonedTrainingPhase = objectMapper.convertValue(trainingPhase, TrainingPhase.class);
+        clonedTrainingPhase.setId(null);
+        clonedTrainingPhase.setDecisionMatrix(trainingPhase.getDecisionMatrix()
+                .stream()
+                .map(matrix -> objectMapper.convertValue(matrix, DecisionMatrixRow.class))
+                .collect(Collectors.toList()));
+        clonedTrainingPhase.setTasks(trainingPhase.getTasks()
+                .stream()
+                .map(task -> cloneTask(task, clonedTrainingPhase))
+                .collect(Collectors.toList()));
+        clonedTrainingPhase.setTrainingDefinition(trainingDefinition);
+        trainingPhaseRepository.save(clonedTrainingPhase);
+    }
+
+    private Task cloneTask(Task originalTask, TrainingPhase trainingPhase) {
+        Task clonedTask = objectMapper.convertValue(originalTask, Task.class);
+        clonedTask.setTrainingPhase(trainingPhase);
+        return clonedTask;
+    }
+
+    private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) {
+        if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    ARCHIVED_OR_RELEASED));
+        }
+        if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(),
+                    "Cannot update training definition with already created training instance. " +
+                            "Remove training instance/s before updating training definition."));
+        }
+    }
+
+    private void deletePhase(AbstractPhase abstractPhase) {
+        if (abstractPhase instanceof QuestionnairePhase) {
+            questionnairePhaseRepository.delete((QuestionnairePhase) abstractPhase);
+        } else if (abstractPhase instanceof InfoPhase) {
+            infoPhaseRepository.delete((InfoPhase) abstractPhase);
+        } else {
+            trainingPhaseRepository.delete((TrainingPhase) abstractPhase);
+        }
+    }
+
+    private LocalDateTime getCurrentTimeInUTC() {
+        return LocalDateTime.now(Clock.systemUTC());
+    }
+
+    private void addLoggedInUserToTrainingDefinitionAsAuthor(TrainingDefinition trainingDefinition) {
+        Optional<UserRef> user = userRefRepository.findUserByUserRefId(securityService.getUserRefIdFromUserAndGroup());
+        if (user.isPresent()) {
+            trainingDefinition.addAuthor(user.get());
+        } else {
+            UserRef newUser = securityService.createUserRefEntityByInfoFromUserAndGroup();
+            trainingDefinition.addAuthor(newUser);
+        }
+        trainingDefinition.setLastEdited(getCurrentTimeInUTC());
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java
new file mode 100644
index 0000000000000000000000000000000000000000..2fe455f074ee4168dfc643a580f4970e0dc21ede
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java
@@ -0,0 +1,305 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import com.querydsl.core.types.Predicate;
+import cz.muni.ics.kypo.training.adaptive.domain.AccessToken;
+import cz.muni.ics.kypo.training.adaptive.domain.UserRef;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.LockedPoolInfo;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.PoolInfoDTO;
+import cz.muni.ics.kypo.training.adaptive.exceptions.*;
+import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.AccessTokenRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository;
+import cz.muni.ics.kypo.training.adaptive.service.api.SandboxServiceApi;
+import org.apache.commons.lang3.RandomStringUtils;
+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.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * The type Training instance service.
+ */
+@Service
+public class TrainingInstanceService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TrainingInstanceService.class);
+
+    private TrainingInstanceRepository trainingInstanceRepository;
+    private TrainingRunRepository trainingRunRepository;
+    private AccessTokenRepository accessTokenRepository;
+    private UserRefRepository organizerRefRepository;
+    private SecurityService securityService;
+    private final Random random = new Random();
+
+    /**
+     * Instantiates a new Training instance service.
+     *
+     * @param trainingInstanceRepository the training instance repository
+     * @param accessTokenRepository      the access token repository
+     * @param trainingRunRepository      the training run repository
+     * @param organizerRefRepository     the organizer ref repository
+     * @param securityService            the security service
+     */
+    @Autowired
+
+    public TrainingInstanceService(TrainingInstanceRepository trainingInstanceRepository,
+                                   AccessTokenRepository accessTokenRepository,
+                                   TrainingRunRepository trainingRunRepository,
+                                   UserRefRepository organizerRefRepository,
+                                   SecurityService securityService) {
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.trainingRunRepository = trainingRunRepository;
+        this.accessTokenRepository = accessTokenRepository;
+        this.organizerRefRepository = organizerRefRepository;
+        this.securityService = securityService;
+    }
+
+    /**
+     * Finds basic info about Training Instance by id
+     *
+     * @param instanceId of a Training Instance that would be returned
+     * @return specific {@link TrainingInstance} by id
+     * @throws EntityNotFoundException training instance is not found.
+     */
+    public TrainingInstance findById(Long instanceId) {
+        return trainingInstanceRepository.findById(instanceId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(), instanceId)));
+    }
+
+    /**
+     * Find specific Training instance by id including its associated Training definition.
+     *
+     * @param instanceId the instance id
+     * @return the {@link TrainingInstance}
+     */
+    public TrainingInstance findByIdIncludingDefinition(Long instanceId) {
+        return trainingInstanceRepository.findByIdIncludingDefinition(instanceId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(), instanceId)));
+    }
+
+    /**
+     * Find all Training Instances.
+     *
+     * @param predicate represents a predicate (boolean-valued function) of one argument.
+     * @param pageable  pageable parameter with information about pagination.
+     * @return all {@link TrainingInstance}s
+     */
+    public Page<TrainingInstance> findAll(Predicate predicate, Pageable pageable) {
+        return trainingInstanceRepository.findAll(predicate, pageable);
+    }
+
+    /**
+     * Find all training instances based on the logged in user.
+     *
+     * @param predicate      the predicate
+     * @param pageable       the pageable
+     * @param loggedInUserId the logged in user id
+     * @return the page
+     */
+    public Page<TrainingInstance> findAll(Predicate predicate, Pageable pageable, Long loggedInUserId) {
+        return trainingInstanceRepository.findAll(predicate, pageable, loggedInUserId);
+    }
+
+    /**
+     * Creates new training instance
+     *
+     * @param trainingInstance to be created
+     * @return created {@link TrainingInstance}
+     */
+    public TrainingInstance create(TrainingInstance trainingInstance) {
+        trainingInstance.setAccessToken(generateAccessToken(trainingInstance.getAccessToken()));
+        if (trainingInstance.getStartTime().isAfter(trainingInstance.getEndTime())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(),
+                    "End time must be later than start time."));
+        }
+        addLoggedInUserAsOrganizerToTrainingInstance(trainingInstance);
+        return trainingInstanceRepository.save(trainingInstance);
+    }
+
+    /**
+     * updates training instance
+     *
+     * @param trainingInstanceToUpdate to be updated
+     * @return new access token if it was changed
+     * @throws EntityNotFoundException training instance is not found.
+     * @throws EntityConflictException cannot be updated for some reason.
+     */
+    public String update(TrainingInstance trainingInstanceToUpdate) {
+        validateStartAndEndTime(trainingInstanceToUpdate);
+        TrainingInstance trainingInstance = findById(trainingInstanceToUpdate.getId());
+        //add original organizers and poolId to update
+        trainingInstanceToUpdate.setOrganizers(new HashSet<>(trainingInstance.getOrganizers()));
+        addLoggedInUserAsOrganizerToTrainingInstance(trainingInstanceToUpdate);
+        trainingInstanceToUpdate.setPoolId(trainingInstance.getPoolId());
+        //check if TI is running, true - only title can be changed, false - any field can be changed
+        if (LocalDateTime.now(Clock.systemUTC()).isAfter(trainingInstance.getStartTime())) {
+            this.checkChangedFieldsOfRunningTrainingInstance(trainingInstanceToUpdate, trainingInstance);
+        } else {
+            //check if new access token should be generated, if not original is kept
+            if (shouldGenerateNewToken(trainingInstance.getAccessToken(), trainingInstanceToUpdate.getAccessToken())) {
+                trainingInstanceToUpdate.setAccessToken(generateAccessToken(trainingInstanceToUpdate.getAccessToken()));
+            } else {
+                trainingInstanceToUpdate.setAccessToken(trainingInstance.getAccessToken());
+            }
+        }
+        trainingInstanceRepository.save(trainingInstanceToUpdate);
+        return trainingInstanceToUpdate.getAccessToken();
+    }
+
+    private void validateStartAndEndTime(TrainingInstance trainingInstance) {
+        if (trainingInstance.getStartTime().isAfter(trainingInstance.getEndTime())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id",
+                    trainingInstance.getId().getClass(), trainingInstance.getId(),
+                    "End time must be later than start time."));
+        }
+    }
+
+    private void checkChangedFieldsOfRunningTrainingInstance(TrainingInstance trainingInstanceToUpdate, TrainingInstance currentTrainingInstance) {
+        if (!currentTrainingInstance.getStartTime().equals(trainingInstanceToUpdate.getStartTime())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(),
+                    "The start time of the running training instance cannot be changed. Only title can be updated."));
+        } else if (!currentTrainingInstance.getEndTime().equals(trainingInstanceToUpdate.getEndTime())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(),
+                    "The end time of the running training instance cannot be changed. Only title can be updated."));
+        } else if (!currentTrainingInstance.getAccessToken().equals(trainingInstanceToUpdate.getAccessToken())) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(),
+                    "The access token of the running training instance cannot be changed. Only title can be updated."));
+        }
+    }
+
+    private boolean shouldGenerateNewToken(String originalToken, String newToken) {
+        //new token should not be generated if token in update equals original token or if token in update equals original token without PIN
+        String tokenWithoutPin = originalToken.substring(0, originalToken.length() - 5);
+        return !(newToken.equals(tokenWithoutPin) || originalToken.equals(newToken));
+    }
+
+    private String generateAccessToken(String accessToken) {
+        String newPass = "";
+        boolean generated = false;
+        while (!generated) {
+            int firstNumber = this.random.nextInt(5) + 5;
+            String pin = firstNumber + RandomStringUtils.random(3, false, true);
+            newPass = accessToken + "-" + pin;
+            Optional<AccessToken> pW = accessTokenRepository.findOneByAccessToken(newPass);
+            if (!pW.isPresent()) {
+                generated = true;
+            }
+        }
+        AccessToken newTokenInstance = new AccessToken();
+        newTokenInstance.setAccessToken(newPass);
+        accessTokenRepository.saveAndFlush(newTokenInstance);
+        return newPass;
+    }
+
+    private void addLoggedInUserAsOrganizerToTrainingInstance(TrainingInstance trainingInstance) {
+        Optional<UserRef> authorOfTrainingInstance = organizerRefRepository.findUserByUserRefId(securityService.getUserRefIdFromUserAndGroup());
+        if (authorOfTrainingInstance.isPresent()) {
+            trainingInstance.addOrganizer(authorOfTrainingInstance.get());
+        } else {
+            UserRef userRef = securityService.createUserRefEntityByInfoFromUserAndGroup();
+            trainingInstance.addOrganizer(organizerRefRepository.save(userRef));
+        }
+    }
+
+    /**
+     * deletes training instance
+     *
+     * @param trainingInstance the training instance to be deleted.
+     * @throws EntityNotFoundException training instance is not found.
+     * @throws EntityConflictException cannot be deleted for some reason.
+     */
+    public void delete(TrainingInstance trainingInstance) {
+        trainingInstanceRepository.delete(trainingInstance);
+        LOG.debug("Training instance with id: {} deleted.", trainingInstance.getId());
+    }
+
+    /**
+     * deletes training instance
+     *
+     * @param id the training instance to be deleted.
+     * @throws EntityNotFoundException training instance is not found.
+     * @throws EntityConflictException cannot be deleted for some reason.
+     */
+    public void deleteById(Long id) {
+        trainingInstanceRepository.deleteById(id);
+        LOG.debug("Training instance with id: {} deleted.", id);
+    }
+
+    /**
+     * Update training instance pool training instance.
+     *
+     * @param trainingInstance the training instance
+     * @return the training instance
+     */
+    public TrainingInstance updateTrainingInstancePool(TrainingInstance trainingInstance) {
+        return trainingInstanceRepository.saveAndFlush(trainingInstance);
+    }
+
+    /**
+     * Finds all Training Runs of specific Training Instance.
+     *
+     * @param instanceId id of Training Instance whose Training Runs would be returned.
+     * @param isActive   if isActive attribute is True, only active runs are returned
+     * @param pageable   pageable parameter with information about pagination.
+     * @return {@link TrainingRun}s of specific {@link TrainingInstance}
+     */
+    public Page<TrainingRun> findTrainingRunsByTrainingInstance(Long instanceId, Boolean isActive, Pageable pageable) {
+        // check if instance exists
+        this.findById(instanceId);
+        if (isActive == null) {
+            return trainingRunRepository.findAllByTrainingInstanceId(instanceId, pageable);
+        } else if (isActive) {
+            return trainingRunRepository.findAllActiveByTrainingInstanceId(instanceId, pageable);
+        } else {
+            return trainingRunRepository.findAllInactiveByTrainingInstanceId(instanceId, pageable);
+        }
+    }
+
+    /**
+     * Find UserRefs by userRefId
+     *
+     * @param usersRefId of wanted UserRefs
+     * @return {@link UserRef}s with corresponding userRefIds
+     */
+    public Set<UserRef> findUserRefsByUserRefIds(Set<Long> usersRefId) {
+        return organizerRefRepository.findUsers(usersRefId);
+    }
+
+    /**
+     * Check if instance is finished.
+     *
+     * @param trainingInstanceId the training instance id
+     * @return true if instance is finished, false if not
+     */
+    public boolean checkIfInstanceIsFinished(Long trainingInstanceId) {
+        return trainingInstanceRepository.isFinished(trainingInstanceId, LocalDateTime.now(Clock.systemUTC()));
+    }
+
+    /**
+     * Find specific Training instance by its access token and with start time before current time and ending time after current time
+     *
+     * @param accessToken of Training instance
+     * @return Training instance
+     */
+    public TrainingInstance findByStartTimeAfterAndEndTimeBeforeAndAccessToken(String accessToken) {
+        return trainingInstanceRepository.findByStartTimeAfterAndEndTimeBeforeAndAccessToken(LocalDateTime.now(Clock.systemUTC()), accessToken)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken,
+                        "There is no active game session matching access token.")));
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java
new file mode 100644
index 0000000000000000000000000000000000000000..49c620c1bb8102972c9fb34fb957e833f5a99bc4
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java
@@ -0,0 +1,530 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import com.querydsl.core.types.Predicate;
+import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalWO;
+import cz.muni.ics.kypo.training.adaptive.domain.TRAcquisitionLock;
+import cz.muni.ics.kypo.training.adaptive.domain.UserRef;
+import cz.muni.ics.kypo.training.adaptive.domain.enums.TRState;
+import cz.muni.ics.kypo.training.adaptive.domain.phases.*;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance;
+import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxInfo;
+import cz.muni.ics.kypo.training.adaptive.exceptions.*;
+import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TRAcquisitionLockRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository;
+import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository;
+import cz.muni.ics.kypo.training.adaptive.service.api.ElasticsearchServiceApi;
+import cz.muni.ics.kypo.training.adaptive.service.api.SandboxServiceApi;
+import cz.muni.ics.kypo.training.adaptive.service.audit.AuditEventsService;
+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.dao.DataIntegrityViolationException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The type Training run service.
+ */
+@Service
+public class TrainingRunService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TrainingRunService.class);
+
+    private SandboxServiceApi sandboxServiceApi;
+    private TrainingRunRepository trainingRunRepository;
+    private AbstractPhaseRepository abstractPhaseRepository;
+    private TrainingInstanceRepository trainingInstanceRepository;
+    private UserRefRepository participantRefRepository;
+    private AuditEventsService auditEventsService;
+    private ElasticsearchServiceApi elasticsearchServiceApi;
+    private SecurityService securityService;
+    private TRAcquisitionLockRepository trAcquisitionLockRepository;
+
+    /**
+     * Instantiates a new Training run service.
+     *
+     * @param trainingRunRepository       the training run repository
+     * @param abstractPhaseRepository     the abstract phase repository
+     * @param trainingInstanceRepository  the training instance repository
+     * @param participantRefRepository    the participant ref repository
+     * @param auditEventsService          the audit events service
+     * @param securityService             the security service
+     * @param trAcquisitionLockRepository the tr acquisition lock repository
+     */
+    @Autowired
+    public TrainingRunService(SandboxServiceApi sandboxServiceApi,
+                              TrainingRunRepository trainingRunRepository,
+                              AbstractPhaseRepository abstractPhaseRepository,
+                              TrainingInstanceRepository trainingInstanceRepository,
+                              UserRefRepository participantRefRepository,
+                              AuditEventsService auditEventsService,
+                              ElasticsearchServiceApi elasticsearchServiceApi,
+                              SecurityService securityService,
+                              TRAcquisitionLockRepository trAcquisitionLockRepository) {
+        this.sandboxServiceApi = sandboxServiceApi;
+        this.trainingRunRepository = trainingRunRepository;
+        this.abstractPhaseRepository = abstractPhaseRepository;
+        this.trainingInstanceRepository = trainingInstanceRepository;
+        this.participantRefRepository = participantRefRepository;
+        this.auditEventsService = auditEventsService;
+        this.elasticsearchServiceApi = elasticsearchServiceApi;
+        this.securityService = securityService;
+        this.trAcquisitionLockRepository = trAcquisitionLockRepository;
+    }
+
+    /**
+     * Finds specific Training Run by id.
+     *
+     * @param runId of a Training Run that would be returned
+     * @return specific {@link TrainingRun} by id
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public TrainingRun findById(Long runId) {
+        return trainingRunRepository.findById(runId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId)));
+    }
+
+    /**
+     * /**
+     * Finds specific Training Run by id including current phase.
+     *
+     * @param runId of a Training Run with phase that would be returned
+     * @return specific {@link TrainingRun} by id
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public TrainingRun findByIdWithPhase(Long runId) {
+        return trainingRunRepository.findByIdWithPhase(runId).orElseThrow(() -> new EntityNotFoundException(
+                new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId)));
+    }
+
+    /**
+     * Find all Training Runs.
+     *
+     * @param predicate specifies query to the database.
+     * @param pageable  pageable parameter with information about pagination.
+     * @return all {@link TrainingRun}s
+     */
+    public Page<TrainingRun> findAll(Predicate predicate, Pageable pageable) {
+        return trainingRunRepository.findAll(predicate, pageable);
+    }
+
+    /**
+     * Delete selected training run.
+     *
+     * @param trainingRunId training run to delete
+     * @param forceDelete   delete training run in a force manner
+     */
+    public void deleteTrainingRun(Long trainingRunId, boolean forceDelete) {
+        TrainingRun trainingRun = findById(trainingRunId);
+        if (!forceDelete && trainingRun.getState().equals(TRState.RUNNING)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRun.getId().getClass(), trainingRun.getId(),
+                    "Cannot delete training run that is running. Consider force delete."));
+        }
+        elasticsearchServiceApi.deleteEventsFromTrainingRun(trainingRun.getTrainingInstance().getId(), trainingRunId);
+        trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId());
+        trainingRunRepository.delete(trainingRun);
+    }
+
+    /**
+     * Checks whether any trainin runs exists for particular training instance
+     *
+     * @param trainingInstanceId the training instance id
+     * @return boolean boolean
+     */
+    public boolean existsAnyForTrainingInstance(Long trainingInstanceId) {
+        return trainingRunRepository.existsAnyForTrainingInstance(trainingInstanceId);
+    }
+
+
+    /**
+     * Finds all Training Runs of logged in user.
+     *
+     * @param pageable pageable parameter with information about pagination.
+     * @return {@link TrainingRun}s of logged in user.
+     */
+    public Page<TrainingRun> findAllByParticipantRefUserRefId(Pageable pageable) {
+        return trainingRunRepository.findAllByParticipantRefId(securityService.getUserRefIdFromUserAndGroup(), pageable);
+    }
+
+    /**
+     * Finds all Training Runs of particular training instance.
+     *
+     * @param trainingInstanceId the training instance id
+     * @return the set
+     */
+    public Set<TrainingRun> findAllByTrainingInstanceId(Long trainingInstanceId) {
+        return trainingRunRepository.findAllByTrainingInstanceId(trainingInstanceId);
+    }
+
+    /**
+     * Gets next phase of given Training Run and set new current phase.
+     *
+     * @param runId id of Training Run whose next phase should be returned.
+     * @return {@link AbstractPhase}
+     * @throws EntityNotFoundException training run or phase is not found.
+     */
+    public AbstractPhase getNextPhase(Long runId) {
+        TrainingRun trainingRun = findByIdWithPhase(runId);
+        int currentPhaseOrder = trainingRun.getCurrentPhase().getOrder();
+        int maxPhaseOrder = abstractPhaseRepository.getCurrentMaxOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId());
+        if (!trainingRun.isLevelAnswered()) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId,
+                    "You need to answer the phase to move to the next phase."));
+        }
+        if (currentPhaseOrder == maxPhaseOrder) {
+            throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "There is no next phase for current training run (ID: " + runId + ")."));
+        }
+        List<AbstractPhase> phases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId());
+        int nextPhaseIndex = phases.indexOf(trainingRun.getCurrentPhase()) + 1;
+        AbstractPhase abstractPhase = phases.get(nextPhaseIndex);
+        if (trainingRun.getCurrentPhase() instanceof InfoPhase) {
+            auditEventsService.auditPhaseCompletedAction(trainingRun);
+        }
+        trainingRun.setCurrentPhase(abstractPhase);
+        trainingRun.setIncorrectFlagCount(0);
+        trainingRunRepository.save(trainingRun);
+        auditEventsService.auditPhaseStartedAction(trainingRun);
+
+        return abstractPhase;
+    }
+
+    /**
+     * Finds all Training Runs of specific Training Definition of logged in user.
+     *
+     * @param definitionId id of Training Definition
+     * @param pageable     pageable parameter with information about pagination.
+     * @return {@link TrainingRun}s of specific Training Definition of logged in user
+     */
+    public Page<TrainingRun> findAllByTrainingDefinitionAndParticipant(Long definitionId, Pageable pageable) {
+        return trainingRunRepository.findAllByTrainingDefinitionIdAndParticipantUserRefId(definitionId, securityService.getUserRefIdFromUserAndGroup(), pageable);
+    }
+
+    /**
+     * Finds all Training Runs of specific training definition.
+     *
+     * @param definitionId id of Training Definition whose Training Runs would be returned.
+     * @param pageable     pageable parameter with information about pagination.
+     * @return {@link TrainingRun}s of specific Training Definition
+     */
+    public Page<TrainingRun> findAllByTrainingDefinition(Long definitionId, Pageable pageable) {
+        return trainingRunRepository.findAllByTrainingDefinitionId(definitionId, pageable);
+    }
+
+    /**
+     * Gets list of all phases in Training Definition.
+     *
+     * @param definitionId must be id of first phase of some Training Definition.
+     * @return List of {@link AbstractPhase}s
+     * @throws EntityNotFoundException one of the phases is not found.
+     */
+    public List<AbstractPhase> getPhases(Long definitionId) {
+        return abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId);
+    }
+
+    /**
+     * Access training run based on given accessToken.
+     *
+     * @param trainingInstance the training instance
+     * @param participantRefId the participant ref id
+     * @return accessed {@link TrainingRun}
+     * @throws EntityNotFoundException no active training instance for given access token, no starting phase in training definition.
+     * @throws EntityConflictException pool of sandboxes is not created for training instance.
+     */
+    public TrainingRun createTrainingRun(TrainingInstance trainingInstance, Long participantRefId) {
+        AbstractPhase initialPhase = findFirstPhaseForTrainingRun(trainingInstance.getTrainingDefinition().getId());
+        TrainingRun trainingRun = getNewTrainingRun(initialPhase, trainingInstance, LocalDateTime.now(Clock.systemUTC()), trainingInstance.getEndTime(), participantRefId);
+        return trainingRunRepository.save(trainingRun);
+    }
+
+    /**
+     * Find running training run of user optional.
+     *
+     * @param accessToken      the access token
+     * @param participantRefId the participant ref id
+     * @return the optional
+     */
+    public Optional<TrainingRun> findRunningTrainingRunOfUser(String accessToken, Long participantRefId) {
+        return trainingRunRepository.findRunningTrainingRunOfUser(accessToken, participantRefId);
+    }
+
+    /**
+     * Gets training instance for particular access token.
+     *
+     * @param accessToken the access token
+     * @return the training instance for particular access token
+     */
+    public TrainingInstance getTrainingInstanceForParticularAccessToken(String accessToken) {
+        TrainingInstance trainingInstance = trainingInstanceRepository.findByStartTimeAfterAndEndTimeBeforeAndAccessToken(LocalDateTime.now(Clock.systemUTC()), accessToken)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken,
+                        "There is no active game session matching access token.")));
+        if (trainingInstance.getPoolId() == null) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(),
+                    "At first organizer must allocate sandboxes for training instance."));
+        }
+        return trainingInstance;
+    }
+
+    /**
+     * Tr acquisition lock to prevent many requests from the same user. This method is called in a new transaction that means that the existing one is suspended.
+     *
+     * @param participantRefId   the participant ref id
+     * @param trainingInstanceId the training instance id
+     * @param accessToken        the access token
+     */
+    @TransactionalWO(propagation = Propagation.REQUIRES_NEW)
+    public void trAcquisitionLockToPreventManyRequestsFromSameUser(Long participantRefId, Long trainingInstanceId, String accessToken) {
+        try {
+            trAcquisitionLockRepository.saveAndFlush(new TRAcquisitionLock(participantRefId, trainingInstanceId, LocalDateTime.now(Clock.systemUTC())));
+        } catch (DataIntegrityViolationException ex) {
+            throw new TooManyRequestsException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken,
+                    "Training run has been already accessed and cannot be created again. Please resume Training Run"));
+        }
+    }
+
+    @TransactionalWO(propagation = Propagation.REQUIRES_NEW)
+    public void deleteTrAcquisitionLockToPreventManyRequestsFromSameUser(Long participantRefId, Long trainingInstanceId) {
+        trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(participantRefId, trainingInstanceId);
+    }
+
+    private AbstractPhase findFirstPhaseForTrainingRun(Long trainingDefinitionId) {
+        return abstractPhaseRepository.findFirstPhaseOfTrainingDefinition(trainingDefinitionId)
+                .orElseThrow( () -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, trainingDefinitionId,
+                    "No starting phase available for this training definition.")));
+    }
+
+    private TrainingRun getNewTrainingRun(AbstractPhase currentPhase, TrainingInstance trainingInstance, LocalDateTime startTime, LocalDateTime endTime, Long participantRefId) {
+        TrainingRun newTrainingRun = new TrainingRun();
+        newTrainingRun.setCurrentPhase(currentPhase);
+
+        Optional<UserRef> userRefOpt = participantRefRepository.findUserByUserRefId(participantRefId);
+        if (userRefOpt.isPresent()) {
+            newTrainingRun.setParticipantRef(userRefOpt.get());
+        } else {
+            newTrainingRun.setParticipantRef(participantRefRepository.save(securityService.createUserRefEntityByInfoFromUserAndGroup()));
+        }
+        newTrainingRun.setAssessmentResponses("[]");
+        newTrainingRun.setState(TRState.RUNNING);
+        newTrainingRun.setTrainingInstance(trainingInstance);
+        newTrainingRun.setStartTime(startTime);
+        newTrainingRun.setEndTime(endTime);
+        return newTrainingRun;
+    }
+
+    /**
+     * Connects available sandbox with given Training run.
+     *
+     * @param trainingRun that will be connected with sandbox
+     * @param poolId      the pool id
+     * @return Training run with assigned sandbox
+     * @throws ForbiddenException       no available sandbox.
+     * @throws MicroserviceApiException error calling OpenStack Sandbox Service API
+     */
+    public TrainingRun assignSandbox(TrainingRun trainingRun, long poolId) {
+        Long sandboxInstanceRef = this.sandboxServiceApi.getAndLockSandboxForTrainingRun(poolId);
+        trainingRun.setSandboxInstanceRefId(sandboxInstanceRef);
+        auditEventsService.auditTrainingRunStartedAction(trainingRun);
+        auditEventsService.auditPhaseStartedAction(trainingRun);
+        return trainingRunRepository.save(trainingRun);
+    }
+
+    /**
+     * Resume previously closed training run.
+     *
+     * @param trainingRunId id of training run to be resumed.
+     * @return {@link TrainingRun}
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public TrainingRun resumeTrainingRun(Long trainingRunId) {
+        TrainingRun trainingRun = findByIdWithPhase(trainingRunId);
+        if (trainingRun.getState().equals(TRState.FINISHED) || trainingRun.getState().equals(TRState.ARCHIVED)) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Cannot resume finished training run."));
+        }
+        if (trainingRun.getTrainingInstance().getEndTime().isBefore(LocalDateTime.now(Clock.systemUTC()))) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Cannot resume training run after end of training instance."));
+        }
+        if (trainingRun.getTrainingInstance().getPoolId() == null) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "The pool assignment of the appropriate training instance has been probably canceled. Please contact the organizer."));
+        }
+
+        if (trainingRun.getSandboxInstanceRefId() == null) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Sandbox of this training run was already deleted, you have to start new game."));
+        }
+        auditEventsService.auditTrainingRunResumedAction(trainingRun);
+        return trainingRun;
+    }
+
+    /**
+     * Check given answer of given Training Run.
+     *
+     * @param runId id of Training Run to check answer.
+     * @param answer  string which player submit.
+     * @return true if answer is correct, false if answer is wrong.
+     * @throws EntityNotFoundException training run is not found.
+     * @throws BadRequestException     the current phase of training run is not training phase.
+     */
+    public boolean isCorrectAnswer(Long runId, String answer) {
+        TrainingRun trainingRun = findByIdWithPhase(runId);
+        AbstractPhase currentPhase = trainingRun.getCurrentPhase();
+        if (currentPhase instanceof TrainingPhase) {
+            if (trainingRun.isLevelAnswered()) {
+                throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", Long.class, runId, "The answer of the current phase of training run has been already corrected."));
+            }
+            Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) currentPhase);
+            if (currentTask.getAnswer().equals(answer)) {
+                trainingRun.setLevelAnswered(true);
+                auditEventsService.auditCorrectFlagSubmittedAction(trainingRun, answer);
+                auditEventsService.auditPhaseCompletedAction(trainingRun);
+                return true;
+            } else if (currentTask.getIncorrectAnswerLimit() != trainingRun.getIncorrectFlagCount()) {
+                //TODO add increaseIncorrectAnswerCount
+                trainingRun.setIncorrectFlagCount(trainingRun.getIncorrectFlagCount() + 1);
+            }
+            auditEventsService.auditWrongFlagSubmittedAction(trainingRun, answer);
+            return false;
+        } else {
+            throw new BadRequestException("Current phase is not training phase and does not have answer.");
+        }
+    }
+
+    private Task getCurrentTask(TrainingRun trainingRun, TrainingPhase trainingPhase) {
+        return trainingPhase.getTasks()
+                .stream()
+                .takeWhile(task -> task.getOrder().equals(trainingRun.getCurrentTaskOrder()))
+                .findFirst()
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(Task.class, "order", Integer.class, trainingRun.getCurrentTaskOrder(), "The task in order: " + trainingRun.getCurrentTaskOrder() +
+                        " of training run (ID: " + trainingRun.getId() + ") hasn't been found.")));
+    }
+
+    /**
+     * Gets remaining attempts to solve current phase of training run.
+     *
+     * @param trainingRunId the training run id
+     * @return the remaining attempts
+     */
+    public int getRemainingAttempts(Long trainingRunId) {
+        TrainingRun trainingRun = findByIdWithPhase(trainingRunId);
+        AbstractPhase phase = trainingRun.getCurrentPhase();
+        if (phase instanceof TrainingPhase) {
+            if (trainingRun.isSolutionTaken()) {
+                return 0;
+            }
+            Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) phase);
+            return currentTask.getIncorrectAnswerLimit() - trainingRun.getIncorrectFlagCount();
+        }
+        throw new BadRequestException("Current phase is not training phase and does not have an answer.");
+    }
+
+    /**
+     * Gets solution of current phase of given Training Run.
+     *
+     * @param trainingRunId id of Training Run which current phase gets solution for.
+     * @return solution of current phase.
+     * @throws EntityNotFoundException training run is not found.
+     * @throws BadRequestException     the current phase of training run is not training phase.
+     */
+    public String getSolution(Long trainingRunId) {
+        TrainingRun trainingRun = findByIdWithPhase(trainingRunId);
+        AbstractPhase currentPhase = trainingRun.getCurrentPhase();
+        if (currentPhase instanceof TrainingPhase) {
+            if (!trainingRun.isSolutionTaken()) {
+                trainingRun.setSolutionTaken(true);
+                trainingRunRepository.save(trainingRun);
+                auditEventsService.auditSolutionDisplayedAction(trainingRun);
+            }
+            Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) currentPhase);
+            return currentTask.getSolution();
+        } else {
+            throw new BadRequestException("Current phase is not training phase and does not have solution.");
+        }
+    }
+
+    /**
+     * Gets max phase order of phases from definition.
+     *
+     * @param definitionId id of training definition.
+     * @return max order of phases.
+     */
+    public int getMaxPhaseOrder(Long definitionId) {
+        return abstractPhaseRepository.getCurrentMaxOrder(definitionId);
+    }
+
+    /**
+     * Finish training run.
+     *
+     * @param trainingRunId id of training run to be finished.
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public void finishTrainingRun(Long trainingRunId) {
+        TrainingRun trainingRun = findById(trainingRunId);
+        int maxOrder = abstractPhaseRepository.getCurrentMaxOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId());
+        if (trainingRun.getCurrentPhase().getOrder() != maxOrder) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Cannot finish training run because current phase is not last."));
+        }
+        if (!trainingRun.isLevelAnswered()) {
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Cannot finish training run because current phase is not answered."));
+        }
+        trainingRun.setState(TRState.FINISHED);
+        trainingRun.setEndTime(LocalDateTime.now(Clock.systemUTC()));
+        trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId());
+        if (trainingRun.getCurrentPhase() instanceof InfoPhase) {
+            auditEventsService.auditPhaseCompletedAction(trainingRun);
+        }
+        auditEventsService.auditTrainingRunEndedAction(trainingRun);
+    }
+
+    /**
+     * Archive training run.
+     *
+     * @param trainingRunId id of training run to be archived.
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public void archiveTrainingRun(Long trainingRunId) {
+        TrainingRun trainingRun = findById(trainingRunId);
+        trainingRun.setState(TRState.ARCHIVED);
+        trainingRun.setPreviousSandboxInstanceRefId(trainingRun.getSandboxInstanceRefId());
+        trainingRun.setSandboxInstanceRefId(null);
+        trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId());
+        trainingRunRepository.save(trainingRun);
+    }
+
+    /**
+     * Evaluate and store responses to assessment.
+     *
+     * @param trainingRunId     id of training run to be finished.
+     * @param responsesAsString response to assessment to be evaluated
+     * @throws EntityNotFoundException training run is not found.
+     */
+    public void evaluateResponsesToQuestionnaire(Long trainingRunId, String responsesAsString) {
+        TrainingRun trainingRun = findByIdWithPhase(trainingRunId);
+        if (!(trainingRun.getCurrentPhase() instanceof QuestionnairePhase)) {
+            throw new BadRequestException("Current phase is not questionnaire phase and cannot be evaluated.");
+        }
+        if (trainingRun.isLevelAnswered())
+            throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId,
+                    "Current phase of the training run has been already answered."));
+        //TODO complete the evaluation
+        auditEventsService.auditAssessmentAnswersAction(trainingRun, responsesAsString);
+        auditEventsService.auditPhaseCompletedAction(trainingRun);
+    }
+}
diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ed56879c1cce3eaafdd29868aa2694caaacd630
--- /dev/null
+++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java
@@ -0,0 +1,209 @@
+package cz.muni.ics.kypo.training.adaptive.service.training;
+
+import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalWO;
+import cz.muni.ics.kypo.training.adaptive.domain.UserRef;
+import cz.muni.ics.kypo.training.adaptive.dto.UserRefDTO;
+import cz.muni.ics.kypo.training.adaptive.dto.responses.PageResultResource;
+import cz.muni.ics.kypo.training.adaptive.enums.RoleType;
+import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail;
+import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException;
+import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException;
+import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriBuilder;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * The type User service.
+ */
+@Service
+public class UserService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
+
+    private WebClient userManagementServiceWebClient;
+    private UserRefRepository userRefRepository;
+
+    /**
+     * Instantiates a new User service.
+     *
+     * @param userManagementServiceWebClient the rest template
+     * @param userRefRepository              the user ref repository
+     */
+    public UserService(@Qualifier("userManagementServiceWebClient") WebClient userManagementServiceWebClient,
+                       UserRefRepository userRefRepository) {
+        this.userManagementServiceWebClient = userManagementServiceWebClient;
+        this.userRefRepository = userRefRepository;
+    }
+
+    /**
+     * Finds specific User reference by login
+     *
+     * @param userRefId of wanted User reference
+     * @return {@link UserRef} with corresponding login
+     * @throws EntityNotFoundException UserRef was not found
+     */
+    public UserRef getUserByUserRefId(Long userRefId) {
+        return userRefRepository.findUserByUserRefId(userRefId)
+                .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(UserRef.class, "id", userRefId.getClass(), userRefId)));
+    }
+
+    /**
+     * Finds specific User reference by login
+     *
+     * @param id of wanted User reference
+     * @return {@link UserRef} with corresponding login
+     * @throws EntityNotFoundException UserRef was not found
+     */
+    public UserRefDTO getUserRefDTOByUserRefId(Long id) {
+        try {
+            return userManagementServiceWebClient
+                .get()
+                .uri("/users/{id}", id)
+                .retrieve()
+                .bodyToMono(UserRefDTO.class)
+                .block();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to obtain info about user(ID: " + id + ").", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Gets users with given user ref ids.
+     *
+     * @param userRefIds the user ref ids
+     * @param pageable   pageable parameter with information about pagination.
+     * @param givenName  optional parameter used for filtration
+     * @param familyName optional parameter used for filtration
+     * @return the users with given user ref ids
+     */
+    public PageResultResource<UserRefDTO> getUsersRefDTOByGivenUserIds(Set<Long> userRefIds, Pageable pageable, String givenName, String familyName) {
+        if (userRefIds.isEmpty()) {
+            return new PageResultResource<>(Collections.emptyList(), new PageResultResource.Pagination(0, 0, pageable.getPageSize(), 0, 0));
+        }
+        try {
+            return userManagementServiceWebClient
+                .get()
+                .uri(uriBuilder -> {
+                            uriBuilder
+                                    .path("/users/ids")
+                                    .queryParam("ids", StringUtils.collectionToDelimitedString(userRefIds, ","));
+                            this.setCommonParams(givenName, familyName, pageable, uriBuilder);
+                            return uriBuilder.build();
+                        }
+                )
+                .retrieve()
+                .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {})
+                .block();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to obtain users by IDs: " + userRefIds + ".", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Finds all logins of users that have role of designer
+     *
+     * @param roleType   the role type
+     * @param pageable   pageable parameter with information about pagination.
+     * @param givenName  optional parameter used for filtration
+     * @param familyName optional parameter used for filtration
+     * @return list of users with given role
+     */
+    public PageResultResource<UserRefDTO> getUsersByGivenRole(RoleType roleType, Pageable pageable, String givenName, String familyName) {
+        try {
+            return userManagementServiceWebClient
+                .get()
+                .uri(uriBuilder -> {
+                            uriBuilder
+                                    .path("/roles/users")
+                                    .queryParam("roleType", roleType.name());
+                            this.setCommonParams(givenName, familyName, pageable, uriBuilder);
+                            return uriBuilder.build();
+                        }
+                )
+                .retrieve()
+                .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {})
+                .block();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to obtain users with role " + roleType.name() + ".", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Finds all logins of users that have role of designer
+     *
+     * @param roleType   the role type
+     * @param userRefIds ids of the users who should be excluded from the result set.
+     * @param pageable   the pageable
+     * @param givenName  optional parameter used for filtration
+     * @param familyName optional parameter used for filtration
+     * @return list of users with given role
+     */
+    public PageResultResource<UserRefDTO> getUsersByGivenRoleAndNotWithGivenIds(RoleType roleType, Set<Long> userRefIds, Pageable pageable, String givenName, String familyName) {
+        try {
+            return userManagementServiceWebClient
+                .get()
+                .uri(uriBuilder -> {
+                            uriBuilder
+                                    .path("/roles/users-not-with-ids")
+                                    .queryParam("roleType", roleType.name())
+                                    .queryParam("ids", StringUtils.collectionToDelimitedString(userRefIds, ","));
+                            this.setCommonParams(givenName, familyName, pageable, uriBuilder);
+                            return uriBuilder.build();
+                        }
+                )
+                .retrieve()
+                .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {})
+                .block();
+        } catch (CustomWebClientException ex) {
+            throw new MicroserviceApiException("Error when calling user management service API to obtain users with role " + roleType.name() + " and IDs: " + userRefIds + ".", ex.getApiSubError());
+        }
+    }
+
+    /**
+     * Create new user reference
+     *
+     * @param userRefToCreate user reference to be created
+     * @return created {@link UserRef}
+     */
+    @TransactionalWO
+    public UserRef createUserRef(UserRef userRefToCreate) {
+        UserRef userRef = userRefRepository.save(userRefToCreate);
+        LOG.info("User ref with user_ref_id: {} created.", userRef.getUserRefId());
+        return userRef;
+    }
+
+    private void setCommonParams(String givenName, String familyName, Pageable pageable, UriBuilder builder) {
+        if (givenName != null) {
+            builder.queryParam("givenName", givenName);
+        }
+        if (familyName != null) {
+            builder.queryParam("familyName", familyName);
+        }
+        builder.queryParam("page", pageable.getPageNumber());
+        builder.queryParam("size", pageable.getPageSize());
+    }
+
+    /**
+     * Check if response from external API is not null.
+     *
+     * @param object object to check
+     * @param message exception message if response is null
+     * @throws MicroserviceApiException if response is null
+     */
+    private void checkNonNull(Object object, String message) {
+        if (object == null) {
+            throw new MicroserviceApiException(message);
+        }
+    }
+}