You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by di...@apache.org on 2026/08/07 03:30:33 UTC

(airavata) branch refactor-single-module updated: Adding service and controller layers for application template and deployment

This is an automated email from the ASF dual-hosted git repository.

DImuthuUpe pushed a commit to branch refactor-single-module
in repository https://gitbox.apache.org/repos/asf/airavata.git


The following commit(s) were added to refs/heads/refactor-single-module by this push:
     new 980aa409d9 Adding service and controller layers for application template and deployment
980aa409d9 is described below

commit 980aa409d96f455f454dc7a4becb905c7de624ec
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Thu Aug 6 23:30:27 2026 -0400

    Adding service and controller layers for application template and deployment
---
 .../airavata/api/ApplicationModuleController.java  |  57 ----
 .../api/ApplicationTemplateController.java         |  56 ++++
 .../api/SlurmApplicationDeploymentController.java  |  62 ++++
 .../SlurmApplicationDeploymentRequestDto.java      | 218 +++++++++++++
 .../SlurmApplicationDeploymentResponseDto.java     | 209 +++++++++++++
 .../dto/template/ApplicationTemplateInputDto.java  |  85 +++++
 .../dto/template/ApplicationTemplateOutputDto.java |  60 ++++
 .../template/ApplicationTemplateRequestDto.java    |  56 ++++
 .../template/ApplicationTemplateResponseDto.java   |  56 ++++
 .../application/mapper/ApplicationMapper.java      | 342 ---------------------
 .../mapper/ApplicationTemplateMapper.java          |  34 ++
 .../mapper/SlurmApplicationDeploymentMapper.java   |  28 ++
 .../model/template/ApplicationTemplateEntity.java  |  13 +-
 .../ApplicationDeploymentRepository.java           |  29 --
 .../repository/ApplicationInputRepository.java     |  29 --
 .../repository/ApplicationInterfaceRepository.java |  29 --
 .../repository/ApplicationModuleRepository.java    |  27 --
 .../repository/ApplicationOutputRepository.java    |  27 --
 .../repository/ApplicationTemplateRepository.java  |  11 +
 .../SlurmApplicationDeploymentRepository.java      |  15 +
 .../service/ApplicationInputService.java           |   8 -
 .../service/ApplicationInterfaceService.java       |  18 --
 .../service/ApplicationModuleService.java          |  79 -----
 .../service/ApplicationTemplateService.java        | 117 +++++++
 .../service/SlurmApplicationDeploymentService.java |  88 ++++++
 25 files changed, 1104 insertions(+), 649 deletions(-)

diff --git a/src/main/java/org/apache/airavata/api/ApplicationModuleController.java b/src/main/java/org/apache/airavata/api/ApplicationModuleController.java
deleted file mode 100644
index 516a8f6cd8..0000000000
--- a/src/main/java/org/apache/airavata/api/ApplicationModuleController.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package org.apache.airavata.api;
-
-import java.util.List;
-
-import org.apache.airavata.application.service.ApplicationModuleService;
-import org.apache.airavata.models.appcatalog.appdeployment.ApplicationModule;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.bind.annotation.DeleteMapping;
-
-@RestController
-@RequestMapping("/api/v1/appmodule")
-public class ApplicationModuleController {
-
-    private final ApplicationModuleService applicationModuleService;
-
-    public ApplicationModuleController(ApplicationModuleService applicationModuleService) {
-        this.applicationModuleService = applicationModuleService;
-    }
-
-    @GetMapping("/{moduleId}")
-    @ResponseBody
-    public ApplicationModule getApplicationModule(@PathVariable String moduleId) throws Exception {
-        return applicationModuleService.getApplicationModule(null, moduleId);
-    }
-
-    @GetMapping("")
-    @ResponseBody
-    public List<ApplicationModule> getAllApplicationModules() throws Exception {
-        return applicationModuleService.getAllApplicationModules();
-    }
-
-    @PostMapping("")
-    @ResponseBody
-    public ApplicationModule createApplicationModule(@RequestBody ApplicationModule module) throws Exception {
-        return applicationModuleService.createApplicationModule(null, module);
-    }
-
-    @PostMapping("/{moduleId}")
-    @ResponseBody
-    public ApplicationModule updateApplicationModule(
-            @PathVariable String moduleId, @RequestBody ApplicationModule module) throws Exception {
-        return applicationModuleService.updateApplicationModule(null, moduleId, module);
-    }
-
-    @DeleteMapping("/{moduleId}")
-    @ResponseBody
-    public void deleteApplicationModule(@PathVariable String moduleId) throws Exception {
-        applicationModuleService.deleteApplicationModule(null, moduleId);
-    }
-
-}
diff --git a/src/main/java/org/apache/airavata/api/ApplicationTemplateController.java b/src/main/java/org/apache/airavata/api/ApplicationTemplateController.java
new file mode 100644
index 0000000000..8d5d6c3976
--- /dev/null
+++ b/src/main/java/org/apache/airavata/api/ApplicationTemplateController.java
@@ -0,0 +1,56 @@
+package org.apache.airavata.api;
+
+import jakarta.validation.Valid;
+import java.util.List;
+import org.apache.airavata.application.dto.template.ApplicationTemplateRequestDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateResponseDto;
+import org.apache.airavata.application.service.ApplicationTemplateService;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/application-templates")
+public class ApplicationTemplateController {
+
+    private final ApplicationTemplateService applicationTemplateService;
+
+    public ApplicationTemplateController(ApplicationTemplateService applicationTemplateService) {
+        this.applicationTemplateService = applicationTemplateService;
+    }
+
+    @GetMapping
+    public List<ApplicationTemplateResponseDto> getAllTemplates() {
+        return applicationTemplateService.getAllTemplates();
+    }
+
+    @GetMapping("/{templateId}")
+    public ApplicationTemplateResponseDto getTemplate(@PathVariable String templateId) {
+        return applicationTemplateService.getTemplate(templateId);
+    }
+
+    @PostMapping
+    @ResponseStatus(HttpStatus.CREATED)
+    public ApplicationTemplateResponseDto createTemplate(@Valid @RequestBody ApplicationTemplateRequestDto request) {
+        return applicationTemplateService.createTemplate(request);
+    }
+
+    @PutMapping("/{templateId}")
+    public ApplicationTemplateResponseDto updateTemplate(
+            @PathVariable String templateId, @Valid @RequestBody ApplicationTemplateRequestDto request) {
+        return applicationTemplateService.updateTemplate(templateId, request);
+    }
+
+    @DeleteMapping("/{templateId}")
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void deleteTemplate(@PathVariable String templateId) {
+        applicationTemplateService.deleteTemplate(templateId);
+    }
+}
diff --git a/src/main/java/org/apache/airavata/api/SlurmApplicationDeploymentController.java b/src/main/java/org/apache/airavata/api/SlurmApplicationDeploymentController.java
new file mode 100644
index 0000000000..a44a1c8c98
--- /dev/null
+++ b/src/main/java/org/apache/airavata/api/SlurmApplicationDeploymentController.java
@@ -0,0 +1,62 @@
+package org.apache.airavata.api;
+
+import jakarta.validation.Valid;
+import java.util.List;
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentRequestDto;
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentResponseDto;
+import org.apache.airavata.application.service.SlurmApplicationDeploymentService;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/slurm-deployments")
+public class SlurmApplicationDeploymentController {
+
+    private final SlurmApplicationDeploymentService slurmApplicationDeploymentService;
+
+    public SlurmApplicationDeploymentController(SlurmApplicationDeploymentService slurmApplicationDeploymentService) {
+        this.slurmApplicationDeploymentService = slurmApplicationDeploymentService;
+    }
+
+    /** Lists every deployment, or only those of {@code templateId} when supplied. */
+    @GetMapping
+    public List<SlurmApplicationDeploymentResponseDto> getAllDeployments(
+            @RequestParam(required = false) String templateId) {
+        return templateId == null
+                ? slurmApplicationDeploymentService.getAllDeployments()
+                : slurmApplicationDeploymentService.getDeploymentsByTemplate(templateId);
+    }
+
+    @GetMapping("/{deploymentId}")
+    public SlurmApplicationDeploymentResponseDto getDeployment(@PathVariable String deploymentId) {
+        return slurmApplicationDeploymentService.getDeployment(deploymentId);
+    }
+
+    @PostMapping
+    @ResponseStatus(HttpStatus.CREATED)
+    public SlurmApplicationDeploymentResponseDto createDeployment(
+            @Valid @RequestBody SlurmApplicationDeploymentRequestDto request) {
+        return slurmApplicationDeploymentService.createDeployment(request);
+    }
+
+    @PutMapping("/{deploymentId}")
+    public SlurmApplicationDeploymentResponseDto updateDeployment(
+            @PathVariable String deploymentId, @Valid @RequestBody SlurmApplicationDeploymentRequestDto request) {
+        return slurmApplicationDeploymentService.updateDeployment(deploymentId, request);
+    }
+
+    @DeleteMapping("/{deploymentId}")
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void deleteDeployment(@PathVariable String deploymentId) {
+        slurmApplicationDeploymentService.deleteDeployment(deploymentId);
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentRequestDto.java b/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentRequestDto.java
new file mode 100644
index 0000000000..47576f0266
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentRequestDto.java
@@ -0,0 +1,218 @@
+package org.apache.airavata.application.dto.deployment;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+
+/**
+ * Create/update payload for a Slurm deployment of an application template.
+ * Required fields mirror the {@code nullable = false} columns on the entity.
+ */
+public class SlurmApplicationDeploymentRequestDto {
+
+    @NotBlank(message = "Template id cannot be blank")
+    private String templateId;
+
+    private String slurmClusterId;
+
+    @NotBlank(message = "Slurm run section cannot be blank")
+    private String slurmRunSection;
+
+    @NotNull(message = "Wall time cannot be null")
+    @Positive(message = "Wall time must be positive")
+    private Long wallTimeMinutes;
+
+    @NotBlank(message = "Allocation cannot be blank")
+    private String allocation;
+
+    @NotBlank(message = "User cannot be blank")
+    private String user;
+
+    // Slurm resource allocation parameters
+    private Integer cpus;
+    private String mem;
+    private String memPerCpu;
+    private Integer ntasksPerNode;
+    private Integer cpusPerTask;
+    private Integer nodes;
+    private Integer ntasks;
+
+    // GPU related fields
+    private String gres;
+    private Integer gpus;
+    private String memPerGpu;
+    private String cpusPerGpu;
+    private Integer gpusPerNode;
+
+    private String constraints;
+    private String workDir;
+    private String partition;
+
+    public String getTemplateId() {
+        return templateId;
+    }
+
+    public void setTemplateId(String templateId) {
+        this.templateId = templateId;
+    }
+
+    public String getSlurmClusterId() {
+        return slurmClusterId;
+    }
+
+    public void setSlurmClusterId(String slurmClusterId) {
+        this.slurmClusterId = slurmClusterId;
+    }
+
+    public String getSlurmRunSection() {
+        return slurmRunSection;
+    }
+
+    public void setSlurmRunSection(String slurmRunSection) {
+        this.slurmRunSection = slurmRunSection;
+    }
+
+    public Long getWallTimeMinutes() {
+        return wallTimeMinutes;
+    }
+
+    public void setWallTimeMinutes(Long wallTimeMinutes) {
+        this.wallTimeMinutes = wallTimeMinutes;
+    }
+
+    public String getAllocation() {
+        return allocation;
+    }
+
+    public void setAllocation(String allocation) {
+        this.allocation = allocation;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public Integer getCpus() {
+        return cpus;
+    }
+
+    public void setCpus(Integer cpus) {
+        this.cpus = cpus;
+    }
+
+    public String getMem() {
+        return mem;
+    }
+
+    public void setMem(String mem) {
+        this.mem = mem;
+    }
+
+    public String getMemPerCpu() {
+        return memPerCpu;
+    }
+
+    public void setMemPerCpu(String memPerCpu) {
+        this.memPerCpu = memPerCpu;
+    }
+
+    public Integer getNtasksPerNode() {
+        return ntasksPerNode;
+    }
+
+    public void setNtasksPerNode(Integer ntasksPerNode) {
+        this.ntasksPerNode = ntasksPerNode;
+    }
+
+    public Integer getCpusPerTask() {
+        return cpusPerTask;
+    }
+
+    public void setCpusPerTask(Integer cpusPerTask) {
+        this.cpusPerTask = cpusPerTask;
+    }
+
+    public Integer getNodes() {
+        return nodes;
+    }
+
+    public void setNodes(Integer nodes) {
+        this.nodes = nodes;
+    }
+
+    public Integer getNtasks() {
+        return ntasks;
+    }
+
+    public void setNtasks(Integer ntasks) {
+        this.ntasks = ntasks;
+    }
+
+    public String getGres() {
+        return gres;
+    }
+
+    public void setGres(String gres) {
+        this.gres = gres;
+    }
+
+    public Integer getGpus() {
+        return gpus;
+    }
+
+    public void setGpus(Integer gpus) {
+        this.gpus = gpus;
+    }
+
+    public String getMemPerGpu() {
+        return memPerGpu;
+    }
+
+    public void setMemPerGpu(String memPerGpu) {
+        this.memPerGpu = memPerGpu;
+    }
+
+    public String getCpusPerGpu() {
+        return cpusPerGpu;
+    }
+
+    public void setCpusPerGpu(String cpusPerGpu) {
+        this.cpusPerGpu = cpusPerGpu;
+    }
+
+    public Integer getGpusPerNode() {
+        return gpusPerNode;
+    }
+
+    public void setGpusPerNode(Integer gpusPerNode) {
+        this.gpusPerNode = gpusPerNode;
+    }
+
+    public String getConstraints() {
+        return constraints;
+    }
+
+    public void setConstraints(String constraints) {
+        this.constraints = constraints;
+    }
+
+    public String getWorkDir() {
+        return workDir;
+    }
+
+    public void setWorkDir(String workDir) {
+        this.workDir = workDir;
+    }
+
+    public String getPartition() {
+        return partition;
+    }
+
+    public void setPartition(String partition) {
+        this.partition = partition;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentResponseDto.java b/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentResponseDto.java
new file mode 100644
index 0000000000..ce81d4c6d0
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/deployment/SlurmApplicationDeploymentResponseDto.java
@@ -0,0 +1,209 @@
+package org.apache.airavata.application.dto.deployment;
+
+/** Read view of a Slurm deployment; the owning template is referenced by id only. */
+public class SlurmApplicationDeploymentResponseDto {
+
+    private String deploymentId;
+    private String templateId;
+    private String slurmClusterId;
+    private String slurmRunSection;
+    private Long wallTimeMinutes;
+    private String allocation;
+    private String user;
+
+    // Slurm resource allocation parameters
+    private Integer cpus;
+    private String mem;
+    private String memPerCpu;
+    private Integer ntasksPerNode;
+    private Integer cpusPerTask;
+    private Integer nodes;
+    private Integer ntasks;
+
+    // GPU related fields
+    private String gres;
+    private Integer gpus;
+    private String memPerGpu;
+    private String cpusPerGpu;
+    private Integer gpusPerNode;
+
+    private String constraints;
+    private String workDir;
+    private String partition;
+
+    public String getDeploymentId() {
+        return deploymentId;
+    }
+
+    public void setDeploymentId(String deploymentId) {
+        this.deploymentId = deploymentId;
+    }
+
+    public String getTemplateId() {
+        return templateId;
+    }
+
+    public void setTemplateId(String templateId) {
+        this.templateId = templateId;
+    }
+
+    public String getSlurmClusterId() {
+        return slurmClusterId;
+    }
+
+    public void setSlurmClusterId(String slurmClusterId) {
+        this.slurmClusterId = slurmClusterId;
+    }
+
+    public String getSlurmRunSection() {
+        return slurmRunSection;
+    }
+
+    public void setSlurmRunSection(String slurmRunSection) {
+        this.slurmRunSection = slurmRunSection;
+    }
+
+    public Long getWallTimeMinutes() {
+        return wallTimeMinutes;
+    }
+
+    public void setWallTimeMinutes(Long wallTimeMinutes) {
+        this.wallTimeMinutes = wallTimeMinutes;
+    }
+
+    public String getAllocation() {
+        return allocation;
+    }
+
+    public void setAllocation(String allocation) {
+        this.allocation = allocation;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public Integer getCpus() {
+        return cpus;
+    }
+
+    public void setCpus(Integer cpus) {
+        this.cpus = cpus;
+    }
+
+    public String getMem() {
+        return mem;
+    }
+
+    public void setMem(String mem) {
+        this.mem = mem;
+    }
+
+    public String getMemPerCpu() {
+        return memPerCpu;
+    }
+
+    public void setMemPerCpu(String memPerCpu) {
+        this.memPerCpu = memPerCpu;
+    }
+
+    public Integer getNtasksPerNode() {
+        return ntasksPerNode;
+    }
+
+    public void setNtasksPerNode(Integer ntasksPerNode) {
+        this.ntasksPerNode = ntasksPerNode;
+    }
+
+    public Integer getCpusPerTask() {
+        return cpusPerTask;
+    }
+
+    public void setCpusPerTask(Integer cpusPerTask) {
+        this.cpusPerTask = cpusPerTask;
+    }
+
+    public Integer getNodes() {
+        return nodes;
+    }
+
+    public void setNodes(Integer nodes) {
+        this.nodes = nodes;
+    }
+
+    public Integer getNtasks() {
+        return ntasks;
+    }
+
+    public void setNtasks(Integer ntasks) {
+        this.ntasks = ntasks;
+    }
+
+    public String getGres() {
+        return gres;
+    }
+
+    public void setGres(String gres) {
+        this.gres = gres;
+    }
+
+    public Integer getGpus() {
+        return gpus;
+    }
+
+    public void setGpus(Integer gpus) {
+        this.gpus = gpus;
+    }
+
+    public String getMemPerGpu() {
+        return memPerGpu;
+    }
+
+    public void setMemPerGpu(String memPerGpu) {
+        this.memPerGpu = memPerGpu;
+    }
+
+    public String getCpusPerGpu() {
+        return cpusPerGpu;
+    }
+
+    public void setCpusPerGpu(String cpusPerGpu) {
+        this.cpusPerGpu = cpusPerGpu;
+    }
+
+    public Integer getGpusPerNode() {
+        return gpusPerNode;
+    }
+
+    public void setGpusPerNode(Integer gpusPerNode) {
+        this.gpusPerNode = gpusPerNode;
+    }
+
+    public String getConstraints() {
+        return constraints;
+    }
+
+    public void setConstraints(String constraints) {
+        this.constraints = constraints;
+    }
+
+    public String getWorkDir() {
+        return workDir;
+    }
+
+    public void setWorkDir(String workDir) {
+        this.workDir = workDir;
+    }
+
+    public String getPartition() {
+        return partition;
+    }
+
+    public void setPartition(String partition) {
+        this.partition = partition;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateInputDto.java b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateInputDto.java
new file mode 100644
index 0000000000..5a97fe62d9
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateInputDto.java
@@ -0,0 +1,85 @@
+package org.apache.airavata.application.dto.template;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import org.apache.airavata.application.model.template.ApplicationTemplateInputType;
+
+public class ApplicationTemplateInputDto {
+
+    /** Server assigned; ignored on create/update requests. */
+    private String inputId;
+
+    @NotBlank(message = "Input name cannot be blank")
+    private String inputName;
+
+    private String displayName;
+    private String inputDescription;
+
+    @NotNull(message = "Input type cannot be null")
+    private ApplicationTemplateInputType inputType;
+
+    private boolean required;
+
+    /**
+     * Default input as JSON string. For example:
+     * - For a single value: {"value": "defaultValue"}
+     * - For a list of values: {"values": ["defaultValue1", "defaultValue2"]}
+     */
+    private String defaultValue;
+
+    public String getInputId() {
+        return inputId;
+    }
+
+    public void setInputId(String inputId) {
+        this.inputId = inputId;
+    }
+
+    public String getInputName() {
+        return inputName;
+    }
+
+    public void setInputName(String inputName) {
+        this.inputName = inputName;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getInputDescription() {
+        return inputDescription;
+    }
+
+    public void setInputDescription(String inputDescription) {
+        this.inputDescription = inputDescription;
+    }
+
+    public ApplicationTemplateInputType getInputType() {
+        return inputType;
+    }
+
+    public void setInputType(ApplicationTemplateInputType inputType) {
+        this.inputType = inputType;
+    }
+
+    public boolean isRequired() {
+        return required;
+    }
+
+    public void setRequired(boolean required) {
+        this.required = required;
+    }
+
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    public void setDefaultValue(String defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateOutputDto.java b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateOutputDto.java
new file mode 100644
index 0000000000..b1d439b5ec
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateOutputDto.java
@@ -0,0 +1,60 @@
+package org.apache.airavata.application.dto.template;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import org.apache.airavata.application.model.template.ApplicationTemplateOutputType;
+
+public class ApplicationTemplateOutputDto {
+
+    /** Server assigned; ignored on create/update requests. */
+    private String outputId;
+
+    @NotBlank(message = "Output name cannot be blank")
+    private String outputName;
+
+    private String displayName;
+    private String outputDescription;
+
+    @NotNull(message = "Output type cannot be null")
+    private ApplicationTemplateOutputType outputType;
+
+    public String getOutputId() {
+        return outputId;
+    }
+
+    public void setOutputId(String outputId) {
+        this.outputId = outputId;
+    }
+
+    public String getOutputName() {
+        return outputName;
+    }
+
+    public void setOutputName(String outputName) {
+        this.outputName = outputName;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public String getOutputDescription() {
+        return outputDescription;
+    }
+
+    public void setOutputDescription(String outputDescription) {
+        this.outputDescription = outputDescription;
+    }
+
+    public ApplicationTemplateOutputType getOutputType() {
+        return outputType;
+    }
+
+    public void setOutputType(ApplicationTemplateOutputType outputType) {
+        this.outputType = outputType;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateRequestDto.java b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateRequestDto.java
new file mode 100644
index 0000000000..7e5a1e7220
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateRequestDto.java
@@ -0,0 +1,56 @@
+package org.apache.airavata.application.dto.template;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import java.util.List;
+
+/**
+ * Create/update payload for an application template. Inputs and outputs are owned by
+ * the template, so they are declared inline here rather than through separate endpoints;
+ * an update replaces the previously declared sets.
+ */
+public class ApplicationTemplateRequestDto {
+
+    @NotBlank(message = "Template name cannot be blank")
+    private String templateName;
+
+    private String templateDescription;
+
+    @Valid
+    private List<ApplicationTemplateInputDto> inputs;
+
+    @Valid
+    private List<ApplicationTemplateOutputDto> outputs;
+
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    public void setTemplateName(String templateName) {
+        this.templateName = templateName;
+    }
+
+    public String getTemplateDescription() {
+        return templateDescription;
+    }
+
+    public void setTemplateDescription(String templateDescription) {
+        this.templateDescription = templateDescription;
+    }
+
+    public List<ApplicationTemplateInputDto> getInputs() {
+        return inputs;
+    }
+
+    public void setInputs(List<ApplicationTemplateInputDto> inputs) {
+        this.inputs = inputs;
+    }
+
+    public List<ApplicationTemplateOutputDto> getOutputs() {
+        return outputs;
+    }
+
+    public void setOutputs(List<ApplicationTemplateOutputDto> outputs) {
+        this.outputs = outputs;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateResponseDto.java b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateResponseDto.java
new file mode 100644
index 0000000000..799799976a
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/dto/template/ApplicationTemplateResponseDto.java
@@ -0,0 +1,56 @@
+package org.apache.airavata.application.dto.template;
+
+import java.util.List;
+
+/**
+ * Read view of an application template. Deployments are not inlined — they are a
+ * separate aggregate, reachable via {@code GET /api/v1/slurm-deployments?templateId=...}.
+ */
+public class ApplicationTemplateResponseDto {
+
+    private String templateId;
+    private String templateName;
+    private String templateDescription;
+    private List<ApplicationTemplateInputDto> inputs;
+    private List<ApplicationTemplateOutputDto> outputs;
+
+    public String getTemplateId() {
+        return templateId;
+    }
+
+    public void setTemplateId(String templateId) {
+        this.templateId = templateId;
+    }
+
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    public void setTemplateName(String templateName) {
+        this.templateName = templateName;
+    }
+
+    public String getTemplateDescription() {
+        return templateDescription;
+    }
+
+    public void setTemplateDescription(String templateDescription) {
+        this.templateDescription = templateDescription;
+    }
+
+    public List<ApplicationTemplateInputDto> getInputs() {
+        return inputs;
+    }
+
+    public void setInputs(List<ApplicationTemplateInputDto> inputs) {
+        this.inputs = inputs;
+    }
+
+    public List<ApplicationTemplateOutputDto> getOutputs() {
+        return outputs;
+    }
+
+    public void setOutputs(List<ApplicationTemplateOutputDto> outputs) {
+        this.outputs = outputs;
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/mapper/ApplicationMapper.java b/src/main/java/org/apache/airavata/application/mapper/ApplicationMapper.java
deleted file mode 100644
index 7043ca2de2..0000000000
--- a/src/main/java/org/apache/airavata/application/mapper/ApplicationMapper.java
+++ /dev/null
@@ -1,342 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.mapper;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.airavata.common.CommonMapperConversions;
-import org.apache.airavata.models.appcatalog.appdeployment.ApplicationDeploymentDescription;
-import org.apache.airavata.models.appcatalog.appdeployment.ApplicationModule;
-import org.apache.airavata.models.appcatalog.appdeployment.CommandObject;
-import org.apache.airavata.models.appcatalog.appdeployment.SetEnvPaths;
-import org.apache.airavata.models.appcatalog.appinterface.ApplicationInterfaceDescription;
-import org.apache.airavata.models.application.io.DataType;
-import org.apache.airavata.models.application.io.InputDataObjectType;
-import org.apache.airavata.models.application.io.OutputDataObjectType;
-import org.apache.airavata.application.model.AppIoParamEntity;
-import org.apache.airavata.application.model.ApplicationDeploymentEntity;
-import org.apache.airavata.application.model.ApplicationInterfaceEntity;
-import org.apache.airavata.application.model.ApplicationModuleEntity;
-import org.mapstruct.AfterMapping;
-import org.mapstruct.Mapper;
-import org.mapstruct.MappingTarget;
-import org.mapstruct.ReportingPolicy;
-import org.mapstruct.factory.Mappers;
-
-@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
-public interface ApplicationMapper extends CommonMapperConversions {
-
-    ApplicationMapper INSTANCE = Mappers.getMapper(ApplicationMapper.class);
-
-    // --- ApplicationInterfaceDescription ---
-    ApplicationInterfaceDescription appInterfaceToModel(ApplicationInterfaceEntity entity);
-
-    ApplicationInterfaceEntity appInterfaceToEntity(ApplicationInterfaceDescription model);
-
-    // MapStruct does not match protobuf's repeated accessors to the entity's list
-    // properties,
-    // so it silently drops application_modules (plain strings) and the input/output
-    // collections.
-    @AfterMapping
-    default void afterAppInterfaceToModel(
-            ApplicationInterfaceEntity entity, @MappingTarget ApplicationInterfaceDescription.Builder builder) {
-        if (entity.getApplicationModules() != null) {
-            builder.addAllApplicationModules(entity.getApplicationModules());
-        }
-        if (entity.getApplicationInputs() != null) {
-            for (AppIoParamEntity input : entity.getApplicationInputs()) {
-                builder.addApplicationInputs(appInputToModel(input));
-            }
-        }
-        if (entity.getApplicationOutputs() != null) {
-            for (AppIoParamEntity output : entity.getApplicationOutputs()) {
-                builder.addApplicationOutputs(appOutputToModel(output));
-            }
-        }
-    }
-
-    @AfterMapping
-    default void afterAppInterfaceToEntity(
-            ApplicationInterfaceDescription model, @MappingTarget ApplicationInterfaceEntity entity) {
-        if (!model.applicationModules().isEmpty()) {
-            entity.setApplicationModules(new ArrayList<>(model.applicationModules()));
-        }
-        if (!model.applicationInputs().isEmpty()) {
-            List<AppIoParamEntity> inputs = new ArrayList<>();
-            for (InputDataObjectType input : model.applicationInputs()) {
-                inputs.add(appInputToEntity(input));
-            }
-            entity.setApplicationInputs(inputs);
-        }
-        if (!model.applicationOutputs().isEmpty()) {
-            List<AppIoParamEntity> outputs = new ArrayList<>();
-            for (OutputDataObjectType output : model.applicationOutputs()) {
-                outputs.add(appOutputToEntity(output));
-            }
-            entity.setApplicationOutputs(outputs);
-        }
-    }
-
-    // --- ApplicationModule ---
-    ApplicationModule appModuleToModel(ApplicationModuleEntity entity);
-
-    ApplicationModuleEntity appModuleToEntity(ApplicationModule model);
-
-    // --- ApplicationDeploymentDescription ---
-    default ApplicationDeploymentDescription appDeploymentToModel(ApplicationDeploymentEntity entity) {
-        if (entity == null)
-            return null;
-        ApplicationDeploymentDescription.Builder builder = ApplicationDeploymentDescription.newBuilder();
-        if (entity.getAppDeploymentId() != null)
-            builder.setAppDeploymentId(entity.getAppDeploymentId());
-        if (entity.getAppDeploymentDescription() != null)
-            builder.setAppDeploymentDescription(entity.getAppDeploymentDescription());
-        if (entity.getExecutablePath() != null)
-            builder.setExecutablePath(entity.getExecutablePath());
-        if (entity.getComputeHostId() != null)
-            builder.setComputeHostId(entity.getComputeHostId());
-        if (entity.getAppModuleId() != null)
-            builder.setAppModuleId(entity.getAppModuleId());
-        if (entity.getParallelism() != null)
-            builder.setParallelism(entity.getParallelism());
-        if (entity.getDefaultQueueName() != null)
-            builder.setDefaultQueueName(entity.getDefaultQueueName());
-        builder.setDefaultNodeCount(entity.getDefaultNodeCount());
-        builder.setDefaultCpuCount(entity.getDefaultCPUCount());
-        builder.setDefaultWalltime(entity.getDefaultWallTime());
-        builder.setEditableByUser(entity.getEditableByUser());
-        if (entity.getModuleLoadCmds() != null) {
-            builder.addAllModuleLoadCmds(entity.getModuleLoadCmds().stream()
-                    .map(m -> CommandObject.newBuilder()
-                            .setCommand((String) m.getOrDefault("command", ""))
-                            .setCommandOrder(((Number) m.getOrDefault("commandOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        if (entity.getPreJobCommands() != null) {
-            builder.addAllPreJobCommands(entity.getPreJobCommands().stream()
-                    .map(m -> CommandObject.newBuilder()
-                            .setCommand((String) m.getOrDefault("command", ""))
-                            .setCommandOrder(((Number) m.getOrDefault("commandOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        if (entity.getPostJobCommands() != null) {
-            builder.addAllPostJobCommands(entity.getPostJobCommands().stream()
-                    .map(m -> CommandObject.newBuilder()
-                            .setCommand((String) m.getOrDefault("command", ""))
-                            .setCommandOrder(((Number) m.getOrDefault("commandOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        if (entity.getLibPrependPaths() != null) {
-            builder.addAllLibPrependPaths(entity.getLibPrependPaths().stream()
-                    .map(m -> SetEnvPaths.newBuilder()
-                            .setName((String) m.getOrDefault("name", ""))
-                            .setValue((String) m.getOrDefault("value", ""))
-                            .setEnvPathOrder(((Number) m.getOrDefault("envPathOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        if (entity.getLibAppendPaths() != null) {
-            builder.addAllLibAppendPaths(entity.getLibAppendPaths().stream()
-                    .map(m -> SetEnvPaths.newBuilder()
-                            .setName((String) m.getOrDefault("name", ""))
-                            .setValue((String) m.getOrDefault("value", ""))
-                            .setEnvPathOrder(((Number) m.getOrDefault("envPathOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        if (entity.getSetEnvironment() != null) {
-            builder.addAllSetEnvironment(entity.getSetEnvironment().stream()
-                    .map(m -> SetEnvPaths.newBuilder()
-                            .setName((String) m.getOrDefault("name", ""))
-                            .setValue((String) m.getOrDefault("value", ""))
-                            .setEnvPathOrder(((Number) m.getOrDefault("envPathOrder", 0)).intValue())
-                            .build())
-                    .toList());
-        }
-        return builder.build();
-    }
-
-    default ApplicationDeploymentEntity appDeploymentToEntity(ApplicationDeploymentDescription model) {
-        if (model == null)
-            return null;
-        ApplicationDeploymentEntity entity = new ApplicationDeploymentEntity();
-        entity.setAppDeploymentId(model.appDeploymentId());
-        entity.setAppDeploymentDescription(model.appDeploymentDescription());
-        entity.setExecutablePath(model.executablePath());
-        entity.setComputeHostId(model.computeHostId());
-        entity.setAppModuleId(model.appModuleId());
-        entity.setParallelism(model.parallelism());
-        entity.setDefaultQueueName(model.defaultQueueName());
-        entity.setDefaultNodeCount(model.defaultNodeCount());
-        entity.setDefaultCPUCount(model.defaultCpuCount());
-        entity.setDefaultWallTime(model.defaultWalltime());
-        entity.setEditableByUser(model.editableByUser());
-        if (!model.moduleLoadCmds().isEmpty()) {
-            entity.setModuleLoadCmds(model.moduleLoadCmds().stream()
-                    .map(cmd -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("command", cmd.command());
-                        m.put("commandOrder", cmd.commandOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        if (!model.preJobCommands().isEmpty()) {
-            entity.setPreJobCommands(model.preJobCommands().stream()
-                    .map(cmd -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("command", cmd.command());
-                        m.put("commandOrder", cmd.commandOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        if (!model.postJobCommands().isEmpty()) {
-            entity.setPostJobCommands(model.postJobCommands().stream()
-                    .map(cmd -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("command", cmd.command());
-                        m.put("commandOrder", cmd.commandOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        if (!model.libPrependPaths().isEmpty()) {
-            entity.setLibPrependPaths(model.libPrependPaths().stream()
-                    .map(p -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("name", p.name());
-                        m.put("value", p.value());
-                        m.put("envPathOrder", p.envPathOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        if (!model.libAppendPaths().isEmpty()) {
-            entity.setLibAppendPaths(model.libAppendPaths().stream()
-                    .map(p -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("name", p.name());
-                        m.put("value", p.value());
-                        m.put("envPathOrder", p.envPathOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        if (!model.setEnvironment().isEmpty()) {
-            entity.setSetEnvironment(model.setEnvironment().stream()
-                    .map(p -> {
-                        Map<String, Object> m = new LinkedHashMap<>();
-                        m.put("name", p.name());
-                        m.put("value", p.value());
-                        m.put("envPathOrder", p.envPathOrder());
-                        return m;
-                    })
-                    .toList());
-        }
-        return entity;
-    }
-
-    // --- ApplicationInput (AppIoParamEntity, DIRECTION=INPUT) ---
-    default InputDataObjectType appInputToModel(AppIoParamEntity entity) {
-        if (entity == null)
-            return null;
-        return InputDataObjectType.newBuilder()
-                .setName(entity.getName() != null ? entity.getName() : "")
-                .setValue(entity.getValue() != null ? entity.getValue() : "")
-                .setType(entity.getType() != null ? entity.getType() : DataType.DATA_TYPE_UNKNOWN)
-                .setApplicationArgument(entity.getApplicationArgument() != null ? entity.getApplicationArgument() : "")
-                .setStandardInput(entity.isStandardInput())
-                .setUserFriendlyDescription(
-                        entity.getUserFriendlyDescription() != null ? entity.getUserFriendlyDescription() : "")
-                .setMetaData(entity.getMetaData() != null ? entity.getMetaData() : "")
-                .setInputOrder(entity.getInputOrder())
-                .setIsRequired(entity.isIsRequired())
-                .setRequiredToAddedToCommandLine(entity.isRequiredToAddedToCommandLine())
-                .setDataStaged(entity.isDataStaged())
-                .setIsReadOnly(entity.isReadOnly())
-                .setOverrideFilename(entity.getOverrideFilename() != null ? entity.getOverrideFilename() : "")
-                .build();
-    }
-
-    default AppIoParamEntity appInputToEntity(InputDataObjectType model) {
-        if (model == null)
-            return null;
-        AppIoParamEntity entity = new AppIoParamEntity();
-        entity.setDirection("INPUT");
-        entity.setName(model.name());
-        entity.setValue(model.value());
-        entity.setType(model.type());
-        entity.setApplicationArgument(model.applicationArgument());
-        entity.setStandardInput(model.standardInput());
-        entity.setUserFriendlyDescription(model.userFriendlyDescription());
-        entity.setMetaData(model.metaData());
-        entity.setInputOrder(model.inputOrder());
-        entity.setIsRequired(model.isRequired());
-        entity.setRequiredToAddedToCommandLine(model.requiredToAddedToCommandLine());
-        entity.setDataStaged(model.dataStaged());
-        entity.setReadOnly(model.isReadOnly());
-        entity.setOverrideFilename(model.overrideFilename());
-        return entity;
-    }
-
-    // --- ApplicationOutput (AppIoParamEntity, DIRECTION=OUTPUT) ---
-    default OutputDataObjectType appOutputToModel(AppIoParamEntity entity) {
-        if (entity == null)
-            return null;
-        return OutputDataObjectType.newBuilder()
-                .setName(entity.getName() != null ? entity.getName() : "")
-                .setValue(entity.getValue() != null ? entity.getValue() : "")
-                .setType(entity.getType() != null ? entity.getType() : DataType.DATA_TYPE_UNKNOWN)
-                .setApplicationArgument(entity.getApplicationArgument() != null ? entity.getApplicationArgument() : "")
-                .setIsRequired(entity.isIsRequired())
-                .setRequiredToAddedToCommandLine(entity.isRequiredToAddedToCommandLine())
-                .setDataMovement(entity.isDataMovement())
-                .setLocation(entity.getLocation() != null ? entity.getLocation() : "")
-                .setSearchQuery(entity.getSearchQuery() != null ? entity.getSearchQuery() : "")
-                .setOutputStreaming(entity.isOutputStreaming())
-                .setMetaData(entity.getMetaData() != null ? entity.getMetaData() : "")
-                .build();
-    }
-
-    default AppIoParamEntity appOutputToEntity(OutputDataObjectType model) {
-        if (model == null)
-            return null;
-        AppIoParamEntity entity = new AppIoParamEntity();
-        entity.setDirection("OUTPUT");
-        entity.setName(model.name());
-        entity.setValue(model.value());
-        entity.setType(model.type());
-        entity.setApplicationArgument(model.applicationArgument());
-        entity.setIsRequired(model.isRequired());
-        entity.setRequiredToAddedToCommandLine(model.requiredToAddedToCommandLine());
-        entity.setDataMovement(model.dataMovement());
-        entity.setLocation(model.location());
-        entity.setSearchQuery(model.searchQuery());
-        entity.setOutputStreaming(model.outputStreaming());
-        entity.setMetaData(model.metaData());
-        return entity;
-    }
-}
diff --git a/src/main/java/org/apache/airavata/application/mapper/ApplicationTemplateMapper.java b/src/main/java/org/apache/airavata/application/mapper/ApplicationTemplateMapper.java
new file mode 100644
index 0000000000..cf4f94532e
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/mapper/ApplicationTemplateMapper.java
@@ -0,0 +1,34 @@
+package org.apache.airavata.application.mapper;
+
+import org.apache.airavata.application.dto.template.ApplicationTemplateInputDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateOutputDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateResponseDto;
+import org.apache.airavata.application.model.template.ApplicationTemplateEntity;
+import org.apache.airavata.application.model.template.ApplicationTemplateInputEntity;
+import org.apache.airavata.application.model.template.ApplicationTemplateOutputEntity;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * Entity/DTO conversions for the application template aggregate. The back-reference to
+ * the owning template is deliberately unmapped on the child entities — the service wires
+ * it up so both sides of the association stay consistent.
+ */
+@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface ApplicationTemplateMapper {
+
+    ApplicationTemplateResponseDto toResponseDto(ApplicationTemplateEntity entity);
+
+    ApplicationTemplateInputDto toInputDto(ApplicationTemplateInputEntity entity);
+
+    ApplicationTemplateOutputDto toOutputDto(ApplicationTemplateOutputEntity entity);
+
+    @Mapping(target = "inputId", ignore = true)
+    @Mapping(target = "applicationTemplate", ignore = true)
+    ApplicationTemplateInputEntity toInputEntity(ApplicationTemplateInputDto dto);
+
+    @Mapping(target = "outputId", ignore = true)
+    @Mapping(target = "applicationTemplate", ignore = true)
+    ApplicationTemplateOutputEntity toOutputEntity(ApplicationTemplateOutputDto dto);
+}
diff --git a/src/main/java/org/apache/airavata/application/mapper/SlurmApplicationDeploymentMapper.java b/src/main/java/org/apache/airavata/application/mapper/SlurmApplicationDeploymentMapper.java
new file mode 100644
index 0000000000..4f9a9c015d
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/mapper/SlurmApplicationDeploymentMapper.java
@@ -0,0 +1,28 @@
+package org.apache.airavata.application.mapper;
+
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentRequestDto;
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentResponseDto;
+import org.apache.airavata.application.model.deployment.SlurmApplicationDeploymentEntity;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * Entity/DTO conversions for Slurm deployments. The template association is resolved by
+ * the service from {@code templateId}, so it is never mapped straight off the request.
+ */
+@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface SlurmApplicationDeploymentMapper {
+
+    @Mapping(target = "templateId", source = "applicationTemplate.templateId")
+    SlurmApplicationDeploymentResponseDto toResponseDto(SlurmApplicationDeploymentEntity entity);
+
+    @Mapping(target = "deploymentId", ignore = true)
+    @Mapping(target = "applicationTemplate", ignore = true)
+    SlurmApplicationDeploymentEntity toEntity(SlurmApplicationDeploymentRequestDto dto);
+
+    @Mapping(target = "deploymentId", ignore = true)
+    @Mapping(target = "applicationTemplate", ignore = true)
+    void updateEntity(SlurmApplicationDeploymentRequestDto dto, @MappingTarget SlurmApplicationDeploymentEntity entity);
+}
diff --git a/src/main/java/org/apache/airavata/application/model/template/ApplicationTemplateEntity.java b/src/main/java/org/apache/airavata/application/model/template/ApplicationTemplateEntity.java
index b3739d09a6..d068d42ea7 100644
--- a/src/main/java/org/apache/airavata/application/model/template/ApplicationTemplateEntity.java
+++ b/src/main/java/org/apache/airavata/application/model/template/ApplicationTemplateEntity.java
@@ -1,10 +1,12 @@
 package org.apache.airavata.application.model.template;
 
+import jakarta.persistence.CascadeType;
 import jakarta.persistence.Entity;
 import jakarta.persistence.Id;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.OneToMany;
+import java.util.ArrayList;
 import java.util.List;
 import org.apache.airavata.application.model.deployment.SlurmApplicationDeploymentEntity;
 
@@ -17,11 +19,14 @@ public class ApplicationTemplateEntity {
     private String templateName;
     private String templateDescription;
 
-    @OneToMany(mappedBy = "applicationTemplate")
-    private List<ApplicationTemplateInputEntity> inputs;
+    // Inputs and outputs are owned by the template: they are created, replaced and
+    // deleted with it. Deployments are not — they outlive individual edits and are
+    // managed through their own endpoints.
+    @OneToMany(mappedBy = "applicationTemplate", cascade = CascadeType.ALL, orphanRemoval = true)
+    private List<ApplicationTemplateInputEntity> inputs = new ArrayList<>();
 
-    @OneToMany(mappedBy = "applicationTemplate")
-    private List<ApplicationTemplateOutputEntity> outputs;
+    @OneToMany(mappedBy = "applicationTemplate", cascade = CascadeType.ALL, orphanRemoval = true)
+    private List<ApplicationTemplateOutputEntity> outputs = new ArrayList<>();
 
     @OneToMany(mappedBy = "applicationTemplate")
     private List<SlurmApplicationDeploymentEntity> deployments;
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationDeploymentRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationDeploymentRepository.java
deleted file mode 100644
index e9bdef4095..0000000000
--- a/src/main/java/org/apache/airavata/application/repository/ApplicationDeploymentRepository.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.repository;
-
-import org.springframework.data.repository.CrudRepository;
-import org.springframework.stereotype.Component;
-import org.apache.airavata.application.model.ApplicationDeploymentEntity;
-
-@Component
-public interface ApplicationDeploymentRepository extends CrudRepository<ApplicationDeploymentEntity, String> {
-
-}
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationInputRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationInputRepository.java
deleted file mode 100644
index d37b8b34b9..0000000000
--- a/src/main/java/org/apache/airavata/application/repository/ApplicationInputRepository.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.repository;
-
-import org.springframework.data.repository.CrudRepository;
-import org.apache.airavata.application.model.AppIoParamEntity;
-import org.springframework.stereotype.Component;
-
-@Component
-public interface ApplicationInputRepository extends CrudRepository<AppIoParamEntity, String> {
-
-}
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationInterfaceRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationInterfaceRepository.java
deleted file mode 100644
index 2a5787c50a..0000000000
--- a/src/main/java/org/apache/airavata/application/repository/ApplicationInterfaceRepository.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.repository;
-
-import org.apache.airavata.application.model.ApplicationInterfaceEntity;
-import org.springframework.data.repository.CrudRepository;
-import org.springframework.stereotype.Component;
-
-@Component
-public interface ApplicationInterfaceRepository
-        extends CrudRepository<ApplicationInterfaceEntity, String> {
-}
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationModuleRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationModuleRepository.java
deleted file mode 100644
index 4096c6ecb8..0000000000
--- a/src/main/java/org/apache/airavata/application/repository/ApplicationModuleRepository.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.repository;
-
-import org.apache.airavata.application.model.ApplicationModuleEntity;
-import org.springframework.data.repository.CrudRepository;
-
-public interface ApplicationModuleRepository extends CrudRepository<ApplicationModuleEntity, String> {
-
-}
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationOutputRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationOutputRepository.java
deleted file mode 100644
index 037e3d4681..0000000000
--- a/src/main/java/org/apache/airavata/application/repository/ApplicationOutputRepository.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.application.repository;
-
-import org.springframework.data.repository.CrudRepository;
-import org.apache.airavata.application.model.AppIoParamEntity;
-
-public interface ApplicationOutputRepository extends CrudRepository<AppIoParamEntity, String> {
-
-}
diff --git a/src/main/java/org/apache/airavata/application/repository/ApplicationTemplateRepository.java b/src/main/java/org/apache/airavata/application/repository/ApplicationTemplateRepository.java
new file mode 100644
index 0000000000..921feb9f3b
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/repository/ApplicationTemplateRepository.java
@@ -0,0 +1,11 @@
+package org.apache.airavata.application.repository;
+
+import org.apache.airavata.application.model.template.ApplicationTemplateEntity;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ApplicationTemplateRepository extends ListCrudRepository<ApplicationTemplateEntity, String> {
+
+    boolean existsByTemplateName(String templateName);
+}
diff --git a/src/main/java/org/apache/airavata/application/repository/SlurmApplicationDeploymentRepository.java b/src/main/java/org/apache/airavata/application/repository/SlurmApplicationDeploymentRepository.java
new file mode 100644
index 0000000000..29e2964cb1
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/repository/SlurmApplicationDeploymentRepository.java
@@ -0,0 +1,15 @@
+package org.apache.airavata.application.repository;
+
+import java.util.List;
+import org.apache.airavata.application.model.deployment.SlurmApplicationDeploymentEntity;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface SlurmApplicationDeploymentRepository
+        extends ListCrudRepository<SlurmApplicationDeploymentEntity, String> {
+
+    List<SlurmApplicationDeploymentEntity> findByApplicationTemplate_TemplateId(String templateId);
+
+    boolean existsByApplicationTemplate_TemplateId(String templateId);
+}
diff --git a/src/main/java/org/apache/airavata/application/service/ApplicationInputService.java b/src/main/java/org/apache/airavata/application/service/ApplicationInputService.java
deleted file mode 100644
index fbf90dcfd1..0000000000
--- a/src/main/java/org/apache/airavata/application/service/ApplicationInputService.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.apache.airavata.application.service;
-
-import org.springframework.stereotype.Component;
-
-@Component
-public class ApplicationInputService {
-
-}
diff --git a/src/main/java/org/apache/airavata/application/service/ApplicationInterfaceService.java b/src/main/java/org/apache/airavata/application/service/ApplicationInterfaceService.java
deleted file mode 100644
index 54dc8f7bba..0000000000
--- a/src/main/java/org/apache/airavata/application/service/ApplicationInterfaceService.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.apache.airavata.application.service;
-
-import org.apache.airavata.application.mapper.ApplicationMapper;
-import org.apache.airavata.application.model.ApplicationInterfaceEntity;
-import org.apache.airavata.models.appcatalog.appinterface.ApplicationInterfaceDescription;
-import org.springframework.stereotype.Component;
-
-@Component
-public class ApplicationInterfaceService {
-
-    protected ApplicationInterfaceDescription toModel(ApplicationInterfaceEntity entity) {
-        return ApplicationMapper.INSTANCE.appInterfaceToModel(entity);
-    }
-
-    protected ApplicationInterfaceEntity toEntity(ApplicationInterfaceDescription model) {
-        return ApplicationMapper.INSTANCE.appInterfaceToEntity(model);
-    }
-}
diff --git a/src/main/java/org/apache/airavata/application/service/ApplicationModuleService.java b/src/main/java/org/apache/airavata/application/service/ApplicationModuleService.java
deleted file mode 100644
index 0ecbe8ea37..0000000000
--- a/src/main/java/org/apache/airavata/application/service/ApplicationModuleService.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package org.apache.airavata.application.service;
-
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.airavata.application.mapper.ApplicationMapper;
-import org.apache.airavata.application.model.ApplicationModuleEntity;
-import org.apache.airavata.application.repository.ApplicationModuleRepository;
-import org.apache.airavata.common.AiravataUtils;
-import org.apache.airavata.common.RequestContext;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.stereotype.Component;
-import org.apache.airavata.models.appcatalog.appdeployment.ApplicationModule;
-
-@Component
-public class ApplicationModuleService {
-
-    private final ApplicationModuleRepository applicationModuleRepository;
-
-    public ApplicationModuleService(ApplicationModuleRepository applicationModuleRepository) {
-        this.applicationModuleRepository = applicationModuleRepository;
-    }
-
-    public ApplicationModule getApplicationModule(RequestContext requestContext, String moduleId) throws Exception {
-        ApplicationModuleEntity entity = applicationModuleRepository.findById(moduleId)
-                .orElseThrow(() -> new Exception("Application module not found: " + moduleId));
-        return toModel(entity);
-    }
-
-    public List<ApplicationModule> getAllApplicationModules() throws Exception {
-        List<ApplicationModule> modules = new ArrayList<>();
-        applicationModuleRepository.findAll().forEach(entity -> modules.add(toModel(entity)));
-        return modules;
-    }
-
-    public ApplicationModule createApplicationModule(RequestContext requestContext, ApplicationModule module)
-            throws Exception {
-        String moduleId = module.appModuleId() == null || module.appModuleId().isBlank()
-                ? AiravataUtils.getId(module.appModuleName())
-                : module.appModuleId();
-        ApplicationModuleEntity entity = toEntity(module.toBuilder().setAppModuleId(moduleId).build());
-        entity.setGatewayId("default"); // TODO: Set with defaiult gatewayId from requestContext when available
-        Timestamp now = new Timestamp(System.currentTimeMillis());
-        entity.setCreationTime(now);
-        entity.setUpdateTime(now);
-        ApplicationModuleEntity savedEntity = applicationModuleRepository.save(entity);
-        return toModel(savedEntity);
-    }
-
-    public ApplicationModule updateApplicationModule(RequestContext requestContext, String moduleId,
-            ApplicationModule module) throws Exception {
-        ApplicationModuleEntity existingEntity = applicationModuleRepository.findById(moduleId)
-                .orElseThrow(() -> new Exception("Application module not found: " + moduleId));
-        ApplicationModuleEntity updatedEntity = toEntity(module);
-        updatedEntity.setAppModuleId(existingEntity.getAppModuleId()); // Preserve the original ID
-        ApplicationModuleEntity savedEntity = applicationModuleRepository.save(updatedEntity);
-        return toModel(savedEntity);
-    }
-
-    // Demo of the JWT -> role-lookup -> @PreAuthorize wiring: deleting a module now
-    // requires the caller's bearer token to resolve to the ADMIN authority.
-    @PreAuthorize("hasAuthority('ADMIN')")
-    public void deleteApplicationModule(RequestContext requestContext, String moduleId) throws Exception {
-        if (!applicationModuleRepository.existsById(moduleId)) {
-            throw new Exception("Application module not found: " + moduleId);
-        }
-        applicationModuleRepository.deleteById(moduleId);
-    }
-
-    protected ApplicationModule toModel(ApplicationModuleEntity entity) {
-        return ApplicationMapper.INSTANCE.appModuleToModel(entity);
-    }
-
-    protected ApplicationModuleEntity toEntity(ApplicationModule model) {
-        return ApplicationMapper.INSTANCE.appModuleToEntity(model);
-    }
-
-}
diff --git a/src/main/java/org/apache/airavata/application/service/ApplicationTemplateService.java b/src/main/java/org/apache/airavata/application/service/ApplicationTemplateService.java
new file mode 100644
index 0000000000..d9d91c9c37
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/service/ApplicationTemplateService.java
@@ -0,0 +1,117 @@
+package org.apache.airavata.application.service;
+
+import java.util.List;
+import org.apache.airavata.application.dto.template.ApplicationTemplateInputDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateOutputDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateRequestDto;
+import org.apache.airavata.application.dto.template.ApplicationTemplateResponseDto;
+import org.apache.airavata.application.mapper.ApplicationTemplateMapper;
+import org.apache.airavata.application.model.template.ApplicationTemplateEntity;
+import org.apache.airavata.application.model.template.ApplicationTemplateInputEntity;
+import org.apache.airavata.application.model.template.ApplicationTemplateOutputEntity;
+import org.apache.airavata.application.repository.ApplicationTemplateRepository;
+import org.apache.airavata.application.repository.SlurmApplicationDeploymentRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+/**
+ * CRUD for application templates and their owned input/output declarations.
+ *
+ * <p>Reads run inside a transaction so the lazily loaded input/output collections can be
+ * mapped to DTOs before the session closes — {@code spring.jpa.open-in-view=false} means
+ * they cannot be walked from the serialization layer.
+ */
+@Service
+public class ApplicationTemplateService {
+
+    private final ApplicationTemplateRepository templateRepository;
+    private final SlurmApplicationDeploymentRepository deploymentRepository;
+    private final ApplicationTemplateMapper mapper;
+
+    public ApplicationTemplateService(
+            ApplicationTemplateRepository templateRepository,
+            SlurmApplicationDeploymentRepository deploymentRepository,
+            ApplicationTemplateMapper mapper) {
+        this.templateRepository = templateRepository;
+        this.deploymentRepository = deploymentRepository;
+        this.mapper = mapper;
+    }
+
+    @Transactional(readOnly = true)
+    public List<ApplicationTemplateResponseDto> getAllTemplates() {
+        return templateRepository.findAll().stream()
+                .map(mapper::toResponseDto)
+                .toList();
+    }
+
+    @Transactional(readOnly = true)
+    public ApplicationTemplateResponseDto getTemplate(String templateId) {
+        return mapper.toResponseDto(findOrThrow(templateId));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public ApplicationTemplateResponseDto createTemplate(ApplicationTemplateRequestDto request) {
+        ApplicationTemplateEntity entity = new ApplicationTemplateEntity();
+        entity.setTemplateName(request.getTemplateName());
+        entity.setTemplateDescription(request.getTemplateDescription());
+        replaceChildren(entity, request);
+        return mapper.toResponseDto(templateRepository.save(entity));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public ApplicationTemplateResponseDto updateTemplate(String templateId, ApplicationTemplateRequestDto request) {
+        ApplicationTemplateEntity entity = findOrThrow(templateId);
+        entity.setTemplateName(request.getTemplateName());
+        entity.setTemplateDescription(request.getTemplateDescription());
+        replaceChildren(entity, request);
+        return mapper.toResponseDto(templateRepository.save(entity));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public void deleteTemplate(String templateId) {
+        ApplicationTemplateEntity entity = findOrThrow(templateId);
+        if (deploymentRepository.existsByApplicationTemplate_TemplateId(templateId)) {
+            throw new ResponseStatusException(
+                    HttpStatus.CONFLICT, "Template has deployments and cannot be deleted: " + templateId);
+        }
+        templateRepository.delete(entity);
+    }
+
+    private ApplicationTemplateEntity findOrThrow(String templateId) {
+        return templateRepository
+                .findById(templateId)
+                .orElseThrow(() ->
+                        new ResponseStatusException(HttpStatus.NOT_FOUND, "Template not found: " + templateId));
+    }
+
+    /**
+     * Rewrites the owned inputs/outputs in place. The existing collections are mutated
+     * rather than reassigned — swapping the list instance out from under an
+     * orphan-removing association makes Hibernate throw.
+     */
+    private void replaceChildren(ApplicationTemplateEntity entity, ApplicationTemplateRequestDto request) {
+        entity.getInputs().clear();
+        if (request.getInputs() != null) {
+            for (ApplicationTemplateInputDto dto : request.getInputs()) {
+                ApplicationTemplateInputEntity input = mapper.toInputEntity(dto);
+                input.setApplicationTemplate(entity);
+                entity.getInputs().add(input);
+            }
+        }
+
+        entity.getOutputs().clear();
+        if (request.getOutputs() != null) {
+            for (ApplicationTemplateOutputDto dto : request.getOutputs()) {
+                ApplicationTemplateOutputEntity output = mapper.toOutputEntity(dto);
+                output.setApplicationTemplate(entity);
+                entity.getOutputs().add(output);
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/airavata/application/service/SlurmApplicationDeploymentService.java b/src/main/java/org/apache/airavata/application/service/SlurmApplicationDeploymentService.java
new file mode 100644
index 0000000000..5fb17fb8ad
--- /dev/null
+++ b/src/main/java/org/apache/airavata/application/service/SlurmApplicationDeploymentService.java
@@ -0,0 +1,88 @@
+package org.apache.airavata.application.service;
+
+import java.util.List;
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentRequestDto;
+import org.apache.airavata.application.dto.deployment.SlurmApplicationDeploymentResponseDto;
+import org.apache.airavata.application.mapper.SlurmApplicationDeploymentMapper;
+import org.apache.airavata.application.model.deployment.SlurmApplicationDeploymentEntity;
+import org.apache.airavata.application.model.template.ApplicationTemplateEntity;
+import org.apache.airavata.application.repository.ApplicationTemplateRepository;
+import org.apache.airavata.application.repository.SlurmApplicationDeploymentRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+/** CRUD for Slurm deployments of an application template. */
+@Service
+public class SlurmApplicationDeploymentService {
+
+    private final SlurmApplicationDeploymentRepository deploymentRepository;
+    private final ApplicationTemplateRepository templateRepository;
+    private final SlurmApplicationDeploymentMapper mapper;
+
+    public SlurmApplicationDeploymentService(
+            SlurmApplicationDeploymentRepository deploymentRepository,
+            ApplicationTemplateRepository templateRepository,
+            SlurmApplicationDeploymentMapper mapper) {
+        this.deploymentRepository = deploymentRepository;
+        this.templateRepository = templateRepository;
+        this.mapper = mapper;
+    }
+
+    @Transactional(readOnly = true)
+    public List<SlurmApplicationDeploymentResponseDto> getAllDeployments() {
+        return deploymentRepository.findAll().stream().map(mapper::toResponseDto).toList();
+    }
+
+    @Transactional(readOnly = true)
+    public List<SlurmApplicationDeploymentResponseDto> getDeploymentsByTemplate(String templateId) {
+        return deploymentRepository.findByApplicationTemplate_TemplateId(templateId).stream()
+                .map(mapper::toResponseDto)
+                .toList();
+    }
+
+    @Transactional(readOnly = true)
+    public SlurmApplicationDeploymentResponseDto getDeployment(String deploymentId) {
+        return mapper.toResponseDto(findOrThrow(deploymentId));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public SlurmApplicationDeploymentResponseDto createDeployment(SlurmApplicationDeploymentRequestDto request) {
+        SlurmApplicationDeploymentEntity entity = mapper.toEntity(request);
+        entity.setApplicationTemplate(resolveTemplate(request.getTemplateId()));
+        return mapper.toResponseDto(deploymentRepository.save(entity));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public SlurmApplicationDeploymentResponseDto updateDeployment(
+            String deploymentId, SlurmApplicationDeploymentRequestDto request) {
+        SlurmApplicationDeploymentEntity entity = findOrThrow(deploymentId);
+        mapper.updateEntity(request, entity);
+        entity.setApplicationTemplate(resolveTemplate(request.getTemplateId()));
+        return mapper.toResponseDto(deploymentRepository.save(entity));
+    }
+
+    @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")
+    @Transactional
+    public void deleteDeployment(String deploymentId) {
+        deploymentRepository.delete(findOrThrow(deploymentId));
+    }
+
+    private SlurmApplicationDeploymentEntity findOrThrow(String deploymentId) {
+        return deploymentRepository
+                .findById(deploymentId)
+                .orElseThrow(() ->
+                        new ResponseStatusException(HttpStatus.NOT_FOUND, "Deployment not found: " + deploymentId));
+    }
+
+    private ApplicationTemplateEntity resolveTemplate(String templateId) {
+        return templateRepository
+                .findById(templateId)
+                .orElseThrow(() ->
+                        new ResponseStatusException(HttpStatus.NOT_FOUND, "Template not found: " + templateId));
+    }
+}