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/05/18 00:31:43 UTC

(airavata-custos) 03/09: Implemented service layer for compute allocations, allocation resources and mapping

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

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

commit 3f6e4b96fc51ad61fae8cad56ea816542711b5b5
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Sat May 16 20:38:43 2026 -0400

    Implemented service layer for compute allocations, allocation resources and mapping
---
 docs/API-Docs.md                                   | 302 +++++++++++++++++++++
 .../migrations/000003_compute_allocations.down.sql |  19 ++
 .../migrations/000003_compute_allocations.up.sql   |  47 ++++
 ...4_compute_allocation_resource_mappings.down.sql |  18 ++
 ...004_compute_allocation_resource_mappings.up.sql |  32 +++
 internal/server/server.go                          | 111 ++++++++
 .../compute_allocation_resource_mapping_store.go   | 102 +++++++
 .../store/compute_allocation_resource_store.go     |  83 ++++++
 internal/store/compute_allocation_store.go         |  96 +++++++
 internal/store/store.go                            |  46 ++++
 pkg/models/allocation.go                           |  30 +-
 pkg/service/compute_allocation.go                  | 126 +++++++++
 pkg/service/compute_allocation_resource.go         |  98 +++++++
 pkg/service/compute_allocation_resource_mapping.go | 119 ++++++++
 pkg/service/service.go                             |  48 +++-
 15 files changed, 1250 insertions(+), 27 deletions(-)

diff --git a/docs/API-Docs.md b/docs/API-Docs.md
index dacb5364d..f509a3000 100644
--- a/docs/API-Docs.md
+++ b/docs/API-Docs.md
@@ -269,6 +269,286 @@ Retrieve a project by its ID.
 
 ---
 
+## Compute Clusters
+
+A compute cluster represents a physical or logical HPC resource (e.g. a
+Slurm cluster) where allocations can be provisioned.
+
+### `POST /compute-clusters`
+
+Create a new compute cluster.
+
+**Required fields:** `name`
+**Optional fields:** `id` (auto-generated if omitted)
+
+`name` must be unique across compute clusters.
+
+**Request**
+
+```json
+{ "name": "Delta" }
+```
+
+**Response 201**
+
+```json
+{
+  "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+  "name": "Delta"
+}
+```
+
+**Errors**
+
+- `400` — `name` is required.
+- `409` — a compute cluster with this `name` already exists.
+
+---
+
+### `GET /compute-clusters`
+
+List all compute clusters.
+
+**Response 200**
+
+```json
+[
+  { "id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1", "name": "Delta" },
+  { "id": "1d4e6a3b-7c8f-49b2-bd34-7c1f9a4e5d10", "name": "Phoenix" }
+]
+```
+
+---
+
+### `GET /compute-clusters/{id}`
+
+Retrieve a single compute cluster by its ID.
+
+**Errors**
+
+- `404` — no compute cluster matches the supplied ID.
+
+---
+
+## Compute Allocations
+
+A compute allocation grants a project a budget of Service Units (SUs) on a
+specific compute cluster for a bounded time window.
+
+### `POST /compute-allocations`
+
+Create a new compute allocation.
+
+**Required fields:** `project_id`, `name`, `compute_cluster_id`
+**Optional fields:** `id`, `status` (defaults to `ACTIVE`), `initial_su_amount`, `start_time`, `end_time`
+
+Both `project_id` and `compute_cluster_id` must reference existing records.
+`status` is one of `ACTIVE`, `INACTIVE`, `DELETED`.
+
+**Request**
+
+```json
+{
+  "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+  "name": "Q2 2026 Climate Run",
+  "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+  "initial_su_amount": 100000,
+  "start_time": "2026-04-01T00:00:00Z",
+  "end_time":   "2026-06-30T23:59:59Z"
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+  "name": "Q2 2026 Climate Run",
+  "status": "ACTIVE",
+  "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+  "initial_su_amount": 100000,
+  "start_time": "2026-04-01T00:00:00Z",
+  "end_time":   "2026-06-30T23:59:59Z"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, or `project_id` / `compute_cluster_id` does not exist.
+
+---
+
+### `GET /compute-allocations/{id}`
+
+Retrieve a compute allocation by its ID.
+
+**Errors**
+
+- `404` — no compute allocation matches the supplied ID.
+
+---
+
+## Compute Allocation Resources
+
+A compute allocation resource describes a hardware capability (e.g.
+`GPU B200`, `CPU`) that can be attached to one or more allocations.
+
+### `POST /compute-allocation-resources`
+
+Create a new compute allocation resource.
+
+**Required fields:** `name`, `resource_type`
+**Optional fields:** `id`, `resource_amount`
+
+**Request**
+
+```json
+{
+  "name": "GPU B200",
+  "resource_type": "GPU",
+  "resource_amount": 8
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+  "name": "GPU B200",
+  "resource_type": "GPU",
+  "resource_amount": 8
+}
+```
+
+**Errors**
+
+- `400` — `name` or `resource_type` is missing.
+
+---
+
+### `GET /compute-allocation-resources`
+
+List all compute allocation resources.
+
+**Response 200**
+
+```json
+[
+  {
+    "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+    "name": "GPU B200",
+    "resource_type": "GPU",
+    "resource_amount": 8
+  }
+]
+```
+
+---
+
+### `GET /compute-allocation-resources/{id}`
+
+Retrieve a compute allocation resource by its ID.
+
+**Errors**
+
+- `404` — no resource matches the supplied ID.
+
+---
+
+## Compute Allocation ↔ Resource Mappings
+
+A many-to-many join: an allocation can have many resources attached, and a
+resource can be attached to many allocations. Mappings are unique per
+(allocation, resource) pair, and are cascade-deleted when either parent is
+removed.
+
+### `POST /compute-allocations/{id}/resources`
+
+Attach an existing resource to a compute allocation.
+
+**Path parameters:** `{id}` — the compute allocation ID.
+**Required body fields:** `compute_allocation_resource_id`
+
+**Request**
+
+```json
+{ "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff" }
+```
+
+**Response 201**
+
+```json
+{
+  "id": "7e1d2c3b-4a5f-4b6c-9d8e-0011223344ff",
+  "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff"
+}
+```
+
+**Errors**
+
+- `400` — `compute_allocation_resource_id` missing, or either the allocation or the resource does not exist.
+- `409` — this resource is already attached to the allocation.
+
+---
+
+### `DELETE /compute-allocations/{id}/resources/{resourceId}`
+
+Detach a resource from a compute allocation.
+
+**Response 204** — empty body on success.
+
+**Errors**
+
+- `404` — no such mapping exists.
+
+---
+
+### `GET /compute-allocations/{id}/resources`
+
+List every compute allocation resource currently attached to the given
+compute allocation.
+
+**Response 200**
+
+```json
+[
+  {
+    "id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+    "name": "GPU B200",
+    "resource_type": "GPU",
+    "resource_amount": 8
+  }
+]
+```
+
+---
+
+### `GET /compute-allocation-resources/{id}/allocations`
+
+List every compute allocation that has the given resource attached.
+
+**Response 200**
+
+```json
+[
+  {
+    "id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+    "project_id": "3a8c2e7b-9d1f-4f5a-bc02-7a4d9e6c1bb1",
+    "name": "Q2 2026 Climate Run",
+    "status": "ACTIVE",
+    "compute_cluster_id": "9b0a7f1c-2c5d-4e1b-9a0f-22e8a5c2dcb1",
+    "initial_su_amount": 100000,
+    "start_time": "2026-04-01T00:00:00Z",
+    "end_time":   "2026-06-30T23:59:59Z"
+  }
+]
+```
+
+---
+
 ## End-to-end example
 
 ```bash
@@ -289,6 +569,28 @@ PROJ_ID=$(curl -s -X POST $BASE/projects \
   -d "{\"title\":\"Climate Simulation 2026\",\"origination\":\"ACCESS\",\"originated_id\":\"ACCESS-PRJ-9000\",\"project_pi_id\":\"$USER_ID\"}" \
   | jq -r .id)
 
+CLUSTER_ID=$(curl -s -X POST $BASE/compute-clusters \
+  -H 'Content-Type: application/json' \
+  -d '{"name":"Delta"}' | jq -r .id)
+
+ALLOC_ID=$(curl -s -X POST $BASE/compute-allocations \
+  -H 'Content-Type: application/json' \
+  -d "{\"project_id\":\"$PROJ_ID\",\"name\":\"Q2 2026 Climate Run\",\"compute_cluster_id\":\"$CLUSTER_ID\",\"initial_su_amount\":100000}" \
+  | jq -r .id)
+
+RES_ID=$(curl -s -X POST $BASE/compute-allocation-resources \
+  -H 'Content-Type: application/json' \
+  -d '{"name":"GPU B200","resource_type":"GPU","resource_amount":8}' | jq -r .id)
+
+# Attach the resource to the allocation.
+curl -s -X POST $BASE/compute-allocations/$ALLOC_ID/resources \
+  -H 'Content-Type: application/json' \
+  -d "{\"compute_allocation_resource_id\":\"$RES_ID\"}" | jq
+
+# Bidirectional lookups.
+curl -s $BASE/compute-allocations/$ALLOC_ID/resources | jq
+curl -s $BASE/compute-allocation-resources/$RES_ID/allocations | jq
+
 curl -s $BASE/projects/$PROJ_ID | jq
 ```
 
diff --git a/internal/db/migrations/000003_compute_allocations.down.sql b/internal/db/migrations/000003_compute_allocations.down.sql
new file mode 100644
index 000000000..71fc3aadf
--- /dev/null
+++ b/internal/db/migrations/000003_compute_allocations.down.sql
@@ -0,0 +1,19 @@
+-- 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.
+
+DROP TABLE IF EXISTS compute_allocation_resources;
+DROP TABLE IF EXISTS compute_allocations;
diff --git a/internal/db/migrations/000003_compute_allocations.up.sql b/internal/db/migrations/000003_compute_allocations.up.sql
new file mode 100644
index 000000000..68144ad61
--- /dev/null
+++ b/internal/db/migrations/000003_compute_allocations.up.sql
@@ -0,0 +1,47 @@
+-- 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.
+
+CREATE TABLE IF NOT EXISTS compute_allocations
+(
+    id                 VARCHAR(255) NOT NULL,
+    project_id         VARCHAR(255) NOT NULL,
+    name               VARCHAR(255) NOT NULL,
+    status             VARCHAR(64)  NOT NULL,
+    compute_cluster_id VARCHAR(255) NOT NULL,
+    initial_su_amount  BIGINT       NOT NULL DEFAULT 0,
+    start_time         TIMESTAMP(6) NOT NULL,
+    end_time           TIMESTAMP(6) NOT NULL,
+    created_at         TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    updated_at         TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_compute_allocations_project (project_id),
+    KEY idx_compute_allocations_cluster (compute_cluster_id),
+    CONSTRAINT fk_compute_allocations_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE RESTRICT,
+    CONSTRAINT fk_compute_allocations_cluster FOREIGN KEY (compute_cluster_id) REFERENCES compute_clusters (id) ON DELETE RESTRICT
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS compute_allocation_resources
+(
+    id              VARCHAR(255) NOT NULL,
+    name            VARCHAR(255) NOT NULL,
+    resource_type   VARCHAR(64)  NOT NULL,
+    resource_amount BIGINT       NOT NULL DEFAULT 0,
+    created_at      TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    updated_at      TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_compute_allocation_resources_type (resource_type)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql b/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql
new file mode 100644
index 000000000..cfceaf2fd
--- /dev/null
+++ b/internal/db/migrations/000004_compute_allocation_resource_mappings.down.sql
@@ -0,0 +1,18 @@
+-- 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.
+
+DROP TABLE IF EXISTS compute_allocation_resource_mappings;
diff --git a/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql b/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql
new file mode 100644
index 000000000..877cc0ba7
--- /dev/null
+++ b/internal/db/migrations/000004_compute_allocation_resource_mappings.up.sql
@@ -0,0 +1,32 @@
+-- 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.
+
+CREATE TABLE IF NOT EXISTS compute_allocation_resource_mappings
+(
+    id                             VARCHAR(255) NOT NULL,
+    compute_allocation_id          VARCHAR(255) NOT NULL,
+    compute_allocation_resource_id VARCHAR(255) NOT NULL,
+    created_at                     TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    UNIQUE KEY uq_carm_pair (compute_allocation_id, compute_allocation_resource_id),
+    KEY idx_carm_allocation (compute_allocation_id),
+    KEY idx_carm_resource (compute_allocation_resource_id),
+    CONSTRAINT fk_carm_allocation FOREIGN KEY (compute_allocation_id)
+        REFERENCES compute_allocations (id) ON DELETE CASCADE,
+    CONSTRAINT fk_carm_resource FOREIGN KEY (compute_allocation_resource_id)
+        REFERENCES compute_allocation_resources (id) ON DELETE CASCADE
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/server/server.go b/internal/server/server.go
index 47679bee4..5207c724d 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -63,6 +63,18 @@ func (s *Server) routes() {
 	s.mux.HandleFunc("POST /compute-clusters", s.createComputeCluster)
 	s.mux.HandleFunc("GET /compute-clusters", s.listComputeClusters)
 	s.mux.HandleFunc("GET /compute-clusters/{id}", s.getComputeCluster)
+
+	s.mux.HandleFunc("POST /compute-allocations", s.createComputeAllocation)
+	s.mux.HandleFunc("GET /compute-allocations/{id}", s.getComputeAllocation)
+
+	s.mux.HandleFunc("POST /compute-allocation-resources", s.createComputeAllocationResource)
+	s.mux.HandleFunc("GET /compute-allocation-resources", s.listComputeAllocationResources)
+	s.mux.HandleFunc("GET /compute-allocation-resources/{id}", s.getComputeAllocationResource)
+
+	s.mux.HandleFunc("GET /compute-allocations/{id}/resources", s.listResourcesForAllocation)
+	s.mux.HandleFunc("POST /compute-allocations/{id}/resources", s.attachResourceToAllocation)
+	s.mux.HandleFunc("DELETE /compute-allocations/{id}/resources/{resourceId}", s.detachResourceFromAllocation)
+	s.mux.HandleFunc("GET /compute-allocation-resources/{id}/allocations", s.listAllocationsForResource)
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -170,6 +182,105 @@ func (s *Server) listComputeClusters(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, clusters)
 }
 
+func (s *Server) createComputeAllocation(w http.ResponseWriter, r *http.Request) {
+	var a models.ComputeAllocation
+	if err := decodeJSON(r, &a); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocation(r.Context(), &a)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocation(w http.ResponseWriter, r *http.Request) {
+	a, err := s.svc.GetComputeAllocation(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, a)
+}
+
+func (s *Server) createComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
+	var res models.ComputeAllocationResource
+	if err := decodeJSON(r, &res); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationResource(r.Context(), &res)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationResource(w http.ResponseWriter, r *http.Request) {
+	res, err := s.svc.GetComputeAllocationResource(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) listComputeAllocationResources(w http.ResponseWriter, r *http.Request) {
+	resources, err := s.svc.ListComputeAllocationResources(r.Context())
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, resources)
+}
+
+type attachResourceRequest struct {
+	ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
+}
+
+func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r *http.Request) {
+	var body attachResourceRequest
+	if err := decodeJSON(r, &body); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	mapping, err := s.svc.AttachResourceToAllocation(r.Context(), r.PathValue("id"), body.ComputeAllocationResourceID)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusCreated, mapping)
+}
+
+func (s *Server) detachResourceFromAllocation(w http.ResponseWriter, r *http.Request) {
+	if err := s.svc.DetachResourceFromAllocation(r.Context(), r.PathValue("id"), r.PathValue("resourceId")); err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) listResourcesForAllocation(w http.ResponseWriter, r *http.Request) {
+	resources, err := s.svc.ListResourcesForAllocation(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, resources)
+}
+
+func (s *Server) listAllocationsForResource(w http.ResponseWriter, r *http.Request) {
+	allocs, err := s.svc.ListAllocationsForResource(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, allocs)
+}
+
 // LoggingMiddleware logs every request once it completes.
 func LoggingMiddleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/store/compute_allocation_resource_mapping_store.go b/internal/store/compute_allocation_resource_mapping_store.go
new file mode 100644
index 000000000..ca89bbf06
--- /dev/null
+++ b/internal/store/compute_allocation_resource_mapping_store.go
@@ -0,0 +1,102 @@
+// 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 store
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+
+	"github.com/jmoiron/sqlx"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationResourceMappingStore struct {
+	db *sqlx.DB
+}
+
+// NewComputeAllocationResourceMappingStore returns a MySQL-backed
+// ComputeAllocationResourceMappingStore.
+func NewComputeAllocationResourceMappingStore(db *sqlx.DB) ComputeAllocationResourceMappingStore {
+	return &mysqlComputeAllocationResourceMappingStore{db: db}
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) FindByPair(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error) {
+	var m models.ComputeAllocationResourceMapping
+	err := s.db.GetContext(ctx, &m,
+		`SELECT id, compute_allocation_id, compute_allocation_resource_id
+		 FROM compute_allocation_resource_mappings
+		 WHERE compute_allocation_id = ? AND compute_allocation_resource_id = ?`,
+		allocationID, resourceID)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &m, nil
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) FindResourcesByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error) {
+	var resources []models.ComputeAllocationResource
+	err := s.db.SelectContext(ctx, &resources,
+		`SELECT r.id, r.name, r.resource_type, r.resource_amount
+		 FROM compute_allocation_resources r
+		 JOIN compute_allocation_resource_mappings m
+		     ON m.compute_allocation_resource_id = r.id
+		 WHERE m.compute_allocation_id = ?
+		 ORDER BY r.name`, allocationID)
+	if err != nil {
+		return nil, err
+	}
+	return resources, nil
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) FindAllocationsByResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error) {
+	var allocs []models.ComputeAllocation
+	err := s.db.SelectContext(ctx, &allocs,
+		`SELECT a.id, a.project_id, a.name, a.status, a.compute_cluster_id,
+		        a.initial_su_amount, a.start_time, a.end_time
+		 FROM compute_allocations a
+		 JOIN compute_allocation_resource_mappings m
+		     ON m.compute_allocation_id = a.id
+		 WHERE m.compute_allocation_resource_id = ?
+		 ORDER BY a.name`, resourceID)
+	if err != nil {
+		return nil, err
+	}
+	return allocs, nil
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) Create(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error {
+	_, err := tx.ExecContext(ctx,
+		`INSERT INTO compute_allocation_resource_mappings
+		     (id, compute_allocation_id, compute_allocation_resource_id)
+		 VALUES (?, ?, ?)`,
+		m.ID, m.ComputeAllocationID, m.ComputeAllocationResourceID)
+	return err
+}
+
+func (s *mysqlComputeAllocationResourceMappingStore) DeleteByPair(ctx context.Context, tx *sql.Tx, allocationID, resourceID string) error {
+	_, err := tx.ExecContext(ctx,
+		`DELETE FROM compute_allocation_resource_mappings
+		 WHERE compute_allocation_id = ? AND compute_allocation_resource_id = ?`,
+		allocationID, resourceID)
+	return err
+}
diff --git a/internal/store/compute_allocation_resource_store.go b/internal/store/compute_allocation_resource_store.go
new file mode 100644
index 000000000..efbd507a2
--- /dev/null
+++ b/internal/store/compute_allocation_resource_store.go
@@ -0,0 +1,83 @@
+// 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 store
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+
+	"github.com/jmoiron/sqlx"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationResourceStore struct {
+	db *sqlx.DB
+}
+
+// NewComputeAllocationResourceStore returns a MySQL-backed
+// ComputeAllocationResourceStore.
+func NewComputeAllocationResourceStore(db *sqlx.DB) ComputeAllocationResourceStore {
+	return &mysqlComputeAllocationResourceStore{db: db}
+}
+
+func (s *mysqlComputeAllocationResourceStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocationResource, error) {
+	var r models.ComputeAllocationResource
+	err := s.db.GetContext(ctx, &r,
+		`SELECT id, name, resource_type, resource_amount FROM compute_allocation_resources WHERE id = ?`, id)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &r, nil
+}
+
+func (s *mysqlComputeAllocationResourceStore) List(ctx context.Context) ([]models.ComputeAllocationResource, error) {
+	var resources []models.ComputeAllocationResource
+	err := s.db.SelectContext(ctx, &resources,
+		`SELECT id, name, resource_type, resource_amount FROM compute_allocation_resources ORDER BY name`)
+	if err != nil {
+		return nil, err
+	}
+	return resources, nil
+}
+
+func (s *mysqlComputeAllocationResourceStore) Create(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error {
+	_, err := tx.ExecContext(ctx,
+		`INSERT INTO compute_allocation_resources (id, name, resource_type, resource_amount)
+		 VALUES (?, ?, ?, ?)`,
+		r.ID, r.Name, r.ResourceType, r.ResourceAmount)
+	return err
+}
+
+func (s *mysqlComputeAllocationResourceStore) Update(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error {
+	_, err := tx.ExecContext(ctx,
+		`UPDATE compute_allocation_resources
+		 SET name = ?, resource_type = ?, resource_amount = ?
+		 WHERE id = ?`,
+		r.Name, r.ResourceType, r.ResourceAmount, r.ID)
+	return err
+}
+
+func (s *mysqlComputeAllocationResourceStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+	_, err := tx.ExecContext(ctx, `DELETE FROM compute_allocation_resources WHERE id = ?`, id)
+	return err
+}
diff --git a/internal/store/compute_allocation_store.go b/internal/store/compute_allocation_store.go
new file mode 100644
index 000000000..9bfc5c922
--- /dev/null
+++ b/internal/store/compute_allocation_store.go
@@ -0,0 +1,96 @@
+// 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 store
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+
+	"github.com/jmoiron/sqlx"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+type mysqlComputeAllocationStore struct {
+	db *sqlx.DB
+}
+
+// NewComputeAllocationStore returns a MySQL-backed ComputeAllocationStore.
+func NewComputeAllocationStore(db *sqlx.DB) ComputeAllocationStore {
+	return &mysqlComputeAllocationStore{db: db}
+}
+
+const computeAllocationColumns = `id, project_id, name, status, compute_cluster_id, initial_su_amount, start_time, end_time`
+
+func (s *mysqlComputeAllocationStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocation, error) {
+	var a models.ComputeAllocation
+	err := s.db.GetContext(ctx, &a,
+		`SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE id = ?`, id)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &a, nil
+}
+
+func (s *mysqlComputeAllocationStore) FindByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error) {
+	var allocs []models.ComputeAllocation
+	err := s.db.SelectContext(ctx, &allocs,
+		`SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE project_id = ?`, projectID)
+	if err != nil {
+		return nil, err
+	}
+	return allocs, nil
+}
+
+func (s *mysqlComputeAllocationStore) FindByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error) {
+	var allocs []models.ComputeAllocation
+	err := s.db.SelectContext(ctx, &allocs,
+		`SELECT `+computeAllocationColumns+` FROM compute_allocations WHERE compute_cluster_id = ?`, clusterID)
+	if err != nil {
+		return nil, err
+	}
+	return allocs, nil
+}
+
+func (s *mysqlComputeAllocationStore) Create(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error {
+	_, err := tx.ExecContext(ctx,
+		`INSERT INTO compute_allocations (`+computeAllocationColumns+`)
+		 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+		a.ID, a.ProjectID, a.Name, string(a.Status), a.ComputeClusterID, a.InitialSUAmount, a.StartTime, a.EndTime)
+	return err
+}
+
+func (s *mysqlComputeAllocationStore) Update(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error {
+	_, err := tx.ExecContext(ctx,
+		`UPDATE compute_allocations
+		 SET project_id = ?, name = ?, status = ?, compute_cluster_id = ?,
+		     initial_su_amount = ?, start_time = ?, end_time = ?
+		 WHERE id = ?`,
+		a.ProjectID, a.Name, string(a.Status), a.ComputeClusterID,
+		a.InitialSUAmount, a.StartTime, a.EndTime, a.ID)
+	return err
+}
+
+func (s *mysqlComputeAllocationStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+	_, err := tx.ExecContext(ctx, `DELETE FROM compute_allocations WHERE id = ?`, id)
+	return err
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index c9a6525a8..013f948d1 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -85,3 +85,49 @@ type ProjectStore interface {
 	// Delete removes a project by ID within the provided transaction.
 	Delete(ctx context.Context, tx *sql.Tx, id string) error
 }
+
+// ComputeAllocationStore defines persistence operations for compute allocations.
+type ComputeAllocationStore interface {
+	// FindByID returns the allocation with the given ID, or nil if not found.
+	FindByID(ctx context.Context, id string) (*models.ComputeAllocation, error)
+	// FindByProject returns all allocations attached to the given project.
+	FindByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error)
+	// FindByCluster returns all allocations attached to the given compute cluster.
+	FindByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error)
+	// Create inserts a new allocation within the provided transaction.
+	Create(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error
+	// Update replaces mutable fields of an existing allocation within the provided transaction.
+	Update(ctx context.Context, tx *sql.Tx, a *models.ComputeAllocation) error
+	// Delete removes an allocation by ID within the provided transaction.
+	Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
+
+// ComputeAllocationResourceStore defines persistence operations for compute
+// allocation resources (CPU, GPU, etc.).
+type ComputeAllocationResourceStore interface {
+	// FindByID returns the resource with the given ID, or nil if not found.
+	FindByID(ctx context.Context, id string) (*models.ComputeAllocationResource, error)
+	// List returns all compute allocation resources.
+	List(ctx context.Context) ([]models.ComputeAllocationResource, error)
+	// Create inserts a new resource within the provided transaction.
+	Create(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error
+	// Update replaces mutable fields of an existing resource within the provided transaction.
+	Update(ctx context.Context, tx *sql.Tx, r *models.ComputeAllocationResource) error
+	// Delete removes a resource by ID within the provided transaction.
+	Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
+
+// ComputeAllocationResourceMappingStore defines persistence operations for
+// the join table linking compute allocations and compute allocation resources.
+type ComputeAllocationResourceMappingStore interface {
+	// FindByPair returns the mapping for a (allocation, resource) pair, or nil if absent.
+	FindByPair(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error)
+	// FindResourcesByAllocation returns every resource attached to the given allocation.
+	FindResourcesByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error)
+	// FindAllocationsByResource returns every allocation that has the given resource attached.
+	FindAllocationsByResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error)
+	// Create inserts a new mapping within the provided transaction.
+	Create(ctx context.Context, tx *sql.Tx, m *models.ComputeAllocationResourceMapping) error
+	// DeleteByPair removes the mapping for a (allocation, resource) pair within the provided transaction.
+	DeleteByPair(ctx context.Context, tx *sql.Tx, allocationID, resourceID string) error
+}
diff --git a/pkg/models/allocation.go b/pkg/models/allocation.go
index e3c2c5d94..8d879af1a 100644
--- a/pkg/models/allocation.go
+++ b/pkg/models/allocation.go
@@ -16,27 +16,27 @@ type ComputeCluster struct {
 }
 
 type ComputeAllocation struct {
-	ID               string           `json:"id"`
-	ProjectID        string           `json:"project_id"`
-	Name             string           `json:"name"`
-	Status           AllocationStatus `json:"status"`             // ACTIVE, INACTIVE, DELETED, etc.
-	ComputeClusterID string           `json:"compute_cluster_id"` // The ID of the compute cluster where the allocation is provisioned.
-	InitialSUAmount  int64            `json:"initial_su_amount"`  // SUs allocated at the time of allocation creation.
-	StartTime        time.Time        `json:"start_time"`
-	EndTime          time.Time        `json:"end_time"`
+	ID               string           `json:"id"                 db:"id"`
+	ProjectID        string           `json:"project_id"         db:"project_id"`
+	Name             string           `json:"name"               db:"name"`
+	Status           AllocationStatus `json:"status"             db:"status"`             // ACTIVE, INACTIVE, DELETED, etc.
+	ComputeClusterID string           `json:"compute_cluster_id" db:"compute_cluster_id"` // The ID of the compute cluster where the allocation is provisioned.
+	InitialSUAmount  int64            `json:"initial_su_amount"  db:"initial_su_amount"`  // SUs allocated at the time of allocation creation.
+	StartTime        time.Time        `json:"start_time"         db:"start_time"`
+	EndTime          time.Time        `json:"end_time"           db:"end_time"`
 }
 
 type ComputeAllocationResource struct {
-	ID             string `json:"id"`
-	Name           string `json:"name"`            // A human-readable name for the resource, e.g., "GPU B200", "CPU", "GPU RTX6000", etc.
-	ResourceType   string `json:"resource_type"`   // CPU, GPU, etc.
-	ResourceAmount int64  `json:"resource_amount"` // Number of CPUs, GPUs, etc. allocated.
+	ID             string `json:"id"              db:"id"`
+	Name           string `json:"name"            db:"name"`            // A human-readable name for the resource, e.g., "GPU B200", "CPU", "GPU RTX6000", etc.
+	ResourceType   string `json:"resource_type"   db:"resource_type"`   // CPU, GPU, etc.
+	ResourceAmount int64  `json:"resource_amount" db:"resource_amount"` // Number of CPUs, GPUs, etc. allocated.
 }
 
 type ComputeAllocationResourceMapping struct {
-	ID                          string `json:"id"`
-	ComputeAllocationID         string `json:"compute_allocation_id"`
-	ComputeAllocationResourceID string `json:"compute_allocation_resource_id"`
+	ID                          string `json:"id"                             db:"id"`
+	ComputeAllocationID         string `json:"compute_allocation_id"          db:"compute_allocation_id"`
+	ComputeAllocationResourceID string `json:"compute_allocation_resource_id" db:"compute_allocation_resource_id"`
 }
 
 type ComputeAllocationResourceRate struct {
diff --git a/pkg/service/compute_allocation.go b/pkg/service/compute_allocation.go
new file mode 100644
index 000000000..e51a624e4
--- /dev/null
+++ b/pkg/service/compute_allocation.go
@@ -0,0 +1,126 @@
+// 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 service
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocation persists a new compute allocation. The referenced
+// project and compute cluster must already exist. If alloc.ID is empty a new
+// UUID is generated; if Status is empty it defaults to ACTIVE.
+func (s *Service) CreateComputeAllocation(ctx context.Context, alloc *models.ComputeAllocation) (*models.ComputeAllocation, error) {
+	if alloc == nil {
+		return nil, fmt.Errorf("%w: compute allocation is nil", ErrInvalidInput)
+	}
+	if alloc.Name == "" {
+		return nil, fmt.Errorf("%w: compute allocation name is required", ErrInvalidInput)
+	}
+	if alloc.ProjectID == "" {
+		return nil, fmt.Errorf("%w: project_id is required", ErrInvalidInput)
+	}
+	if alloc.ComputeClusterID == "" {
+		return nil, fmt.Errorf("%w: compute_cluster_id is required", ErrInvalidInput)
+	}
+	if alloc.ID == "" {
+		alloc.ID = newID()
+	}
+	if alloc.Status == "" {
+		alloc.Status = models.ACTIVE
+	}
+
+	if proj, err := s.projs.FindByID(ctx, alloc.ProjectID); err != nil {
+		return nil, fmt.Errorf("lookup project: %w", err)
+	} else if proj == nil {
+		return nil, fmt.Errorf("%w: project %q not found", ErrInvalidInput, alloc.ProjectID)
+	}
+
+	if cluster, err := s.clusters.FindByID(ctx, alloc.ComputeClusterID); err != nil {
+		return nil, fmt.Errorf("lookup compute cluster: %w", err)
+	} else if cluster == nil {
+		return nil, fmt.Errorf("%w: compute cluster %q not found", ErrInvalidInput, alloc.ComputeClusterID)
+	}
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.allocs.Create(ctx, tx, alloc)
+	}); err != nil {
+		return nil, fmt.Errorf("create compute allocation: %w", err)
+	}
+	return alloc, nil
+}
+
+// GetComputeAllocation retrieves a compute allocation by its ID. Returns
+// ErrNotFound when no allocation matches.
+func (s *Service) GetComputeAllocation(ctx context.Context, id string) (*models.ComputeAllocation, error) {
+	a, err := s.allocs.FindByID(ctx, id)
+	if err != nil {
+		return nil, fmt.Errorf("get compute allocation: %w", err)
+	}
+	if a == nil {
+		return nil, ErrNotFound
+	}
+	return a, nil
+}
+
+// ListComputeAllocationsByProject returns every compute allocation attached to a project.
+func (s *Service) ListComputeAllocationsByProject(ctx context.Context, projectID string) ([]models.ComputeAllocation, error) {
+	allocs, err := s.allocs.FindByProject(ctx, projectID)
+	if err != nil {
+		return nil, fmt.Errorf("list compute allocations by project: %w", err)
+	}
+	return allocs, nil
+}
+
+// ListComputeAllocationsByCluster returns every compute allocation attached to a cluster.
+func (s *Service) ListComputeAllocationsByCluster(ctx context.Context, clusterID string) ([]models.ComputeAllocation, error) {
+	allocs, err := s.allocs.FindByCluster(ctx, clusterID)
+	if err != nil {
+		return nil, fmt.Errorf("list compute allocations by cluster: %w", err)
+	}
+	return allocs, nil
+}
+
+// UpdateComputeAllocation persists changes to an existing compute allocation.
+func (s *Service) UpdateComputeAllocation(ctx context.Context, alloc *models.ComputeAllocation) error {
+	if alloc == nil || alloc.ID == "" {
+		return fmt.Errorf("%w: compute allocation id is required", ErrInvalidInput)
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.allocs.Update(ctx, tx, alloc)
+	}); err != nil {
+		return fmt.Errorf("update compute allocation: %w", err)
+	}
+	return nil
+}
+
+// DeleteComputeAllocation removes a compute allocation by ID.
+func (s *Service) DeleteComputeAllocation(ctx context.Context, id string) error {
+	if id == "" {
+		return fmt.Errorf("%w: compute allocation id is required", ErrInvalidInput)
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.allocs.Delete(ctx, tx, id)
+	}); err != nil {
+		return fmt.Errorf("delete compute allocation: %w", err)
+	}
+	return nil
+}
diff --git a/pkg/service/compute_allocation_resource.go b/pkg/service/compute_allocation_resource.go
new file mode 100644
index 000000000..433f019d5
--- /dev/null
+++ b/pkg/service/compute_allocation_resource.go
@@ -0,0 +1,98 @@
+// 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 service
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocationResource persists a new compute allocation resource.
+// If resource.ID is empty a new UUID is generated.
+func (s *Service) CreateComputeAllocationResource(ctx context.Context, resource *models.ComputeAllocationResource) (*models.ComputeAllocationResource, error) {
+	if resource == nil {
+		return nil, fmt.Errorf("%w: compute allocation resource is nil", ErrInvalidInput)
+	}
+	if resource.Name == "" {
+		return nil, fmt.Errorf("%w: resource name is required", ErrInvalidInput)
+	}
+	if resource.ResourceType == "" {
+		return nil, fmt.Errorf("%w: resource_type is required", ErrInvalidInput)
+	}
+	if resource.ID == "" {
+		resource.ID = newID()
+	}
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.resources.Create(ctx, tx, resource)
+	}); err != nil {
+		return nil, fmt.Errorf("create compute allocation resource: %w", err)
+	}
+	return resource, nil
+}
+
+// GetComputeAllocationResource retrieves a compute allocation resource by its ID.
+// Returns ErrNotFound when no resource matches.
+func (s *Service) GetComputeAllocationResource(ctx context.Context, id string) (*models.ComputeAllocationResource, error) {
+	r, err := s.resources.FindByID(ctx, id)
+	if err != nil {
+		return nil, fmt.Errorf("get compute allocation resource: %w", err)
+	}
+	if r == nil {
+		return nil, ErrNotFound
+	}
+	return r, nil
+}
+
+// ListComputeAllocationResources returns every compute allocation resource.
+func (s *Service) ListComputeAllocationResources(ctx context.Context) ([]models.ComputeAllocationResource, error) {
+	resources, err := s.resources.List(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("list compute allocation resources: %w", err)
+	}
+	return resources, nil
+}
+
+// UpdateComputeAllocationResource persists changes to an existing resource.
+func (s *Service) UpdateComputeAllocationResource(ctx context.Context, resource *models.ComputeAllocationResource) error {
+	if resource == nil || resource.ID == "" {
+		return fmt.Errorf("%w: compute allocation resource id is required", ErrInvalidInput)
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.resources.Update(ctx, tx, resource)
+	}); err != nil {
+		return fmt.Errorf("update compute allocation resource: %w", err)
+	}
+	return nil
+}
+
+// DeleteComputeAllocationResource removes a compute allocation resource by ID.
+func (s *Service) DeleteComputeAllocationResource(ctx context.Context, id string) error {
+	if id == "" {
+		return fmt.Errorf("%w: compute allocation resource id is required", ErrInvalidInput)
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.resources.Delete(ctx, tx, id)
+	}); err != nil {
+		return fmt.Errorf("delete compute allocation resource: %w", err)
+	}
+	return nil
+}
diff --git a/pkg/service/compute_allocation_resource_mapping.go b/pkg/service/compute_allocation_resource_mapping.go
new file mode 100644
index 000000000..84c4b52e0
--- /dev/null
+++ b/pkg/service/compute_allocation_resource_mapping.go
@@ -0,0 +1,119 @@
+// 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 service
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// AttachResourceToAllocation links a compute allocation resource to a compute
+// allocation. Both entities must already exist. The link is idempotent — if
+// the same (allocation, resource) pair is already linked, ErrAlreadyExists is
+// returned.
+func (s *Service) AttachResourceToAllocation(ctx context.Context, allocationID, resourceID string) (*models.ComputeAllocationResourceMapping, error) {
+	if allocationID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput)
+	}
+	if resourceID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_resource_id is required", ErrInvalidInput)
+	}
+
+	if alloc, err := s.allocs.FindByID(ctx, allocationID); err != nil {
+		return nil, fmt.Errorf("lookup compute allocation: %w", err)
+	} else if alloc == nil {
+		return nil, fmt.Errorf("%w: compute allocation %q not found", ErrInvalidInput, allocationID)
+	}
+
+	if res, err := s.resources.FindByID(ctx, resourceID); err != nil {
+		return nil, fmt.Errorf("lookup compute allocation resource: %w", err)
+	} else if res == nil {
+		return nil, fmt.Errorf("%w: compute allocation resource %q not found", ErrInvalidInput, resourceID)
+	}
+
+	if existing, err := s.resourceMappings.FindByPair(ctx, allocationID, resourceID); err != nil {
+		return nil, fmt.Errorf("lookup mapping: %w", err)
+	} else if existing != nil {
+		return nil, fmt.Errorf("%w: resource %q already attached to allocation %q", ErrAlreadyExists, resourceID, allocationID)
+	}
+
+	mapping := &models.ComputeAllocationResourceMapping{
+		ID:                          newID(),
+		ComputeAllocationID:         allocationID,
+		ComputeAllocationResourceID: resourceID,
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.resourceMappings.Create(ctx, tx, mapping)
+	}); err != nil {
+		return nil, fmt.Errorf("attach resource to allocation: %w", err)
+	}
+	return mapping, nil
+}
+
+// DetachResourceFromAllocation removes the link between a compute allocation
+// and a compute allocation resource. Returns ErrNotFound when no such mapping
+// exists.
+func (s *Service) DetachResourceFromAllocation(ctx context.Context, allocationID, resourceID string) error {
+	if allocationID == "" || resourceID == "" {
+		return fmt.Errorf("%w: allocation and resource ids are required", ErrInvalidInput)
+	}
+
+	existing, err := s.resourceMappings.FindByPair(ctx, allocationID, resourceID)
+	if err != nil {
+		return fmt.Errorf("lookup mapping: %w", err)
+	}
+	if existing == nil {
+		return ErrNotFound
+	}
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.resourceMappings.DeleteByPair(ctx, tx, allocationID, resourceID)
+	}); err != nil {
+		return fmt.Errorf("detach resource from allocation: %w", err)
+	}
+	return nil
+}
+
+// ListResourcesForAllocation returns every compute allocation resource
+// attached to the given compute allocation.
+func (s *Service) ListResourcesForAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationResource, error) {
+	if allocationID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput)
+	}
+	resources, err := s.resourceMappings.FindResourcesByAllocation(ctx, allocationID)
+	if err != nil {
+		return nil, fmt.Errorf("list resources for allocation: %w", err)
+	}
+	return resources, nil
+}
+
+// ListAllocationsForResource returns every compute allocation that has the
+// given compute allocation resource attached.
+func (s *Service) ListAllocationsForResource(ctx context.Context, resourceID string) ([]models.ComputeAllocation, error) {
+	if resourceID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_resource_id is required", ErrInvalidInput)
+	}
+	allocs, err := s.resourceMappings.FindAllocationsByResource(ctx, resourceID)
+	if err != nil {
+		return nil, fmt.Errorf("list allocations for resource: %w", err)
+	}
+	return allocs, nil
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index b5926858e..e249fbe7e 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -33,30 +33,54 @@ import (
 // mutating operation in a transaction so callers do not need to manage
 // *sql.Tx themselves.
 type Service struct {
-	db       *sqlx.DB
-	orgs     store.OrganizationStore
-	users    store.UserStore
-	projs    store.ProjectStore
-	clusters store.ComputeClusterStore
+	db               *sqlx.DB
+	orgs             store.OrganizationStore
+	users            store.UserStore
+	projs            store.ProjectStore
+	clusters         store.ComputeClusterStore
+	allocs           store.ComputeAllocationStore
+	resources        store.ComputeAllocationResourceStore
+	resourceMappings store.ComputeAllocationResourceMappingStore
 }
 
 // New constructs a Service backed by the supplied database handle.
 // Stores are instantiated internally using the default MySQL implementations.
 func New(database *sqlx.DB) *Service {
 	return &Service{
-		db:       database,
-		orgs:     store.NewOrganizationStore(database),
-		users:    store.NewUserStore(database),
-		projs:    store.NewProjectStore(database),
-		clusters: store.NewComputeClusterStore(database),
+		db:               database,
+		orgs:             store.NewOrganizationStore(database),
+		users:            store.NewUserStore(database),
+		projs:            store.NewProjectStore(database),
+		clusters:         store.NewComputeClusterStore(database),
+		allocs:           store.NewComputeAllocationStore(database),
+		resources:        store.NewComputeAllocationResourceStore(database),
+		resourceMappings: store.NewComputeAllocationResourceMappingStore(database),
 	}
 }
 
 // NewWithStores constructs a Service from explicit stores. Useful for tests
 // within this module — stores are an internal type and cannot be supplied by
 // external callers.
-func NewWithStores(database *sqlx.DB, orgs store.OrganizationStore, users store.UserStore, projs store.ProjectStore, clusters store.ComputeClusterStore) *Service {
-	return &Service{db: database, orgs: orgs, users: users, projs: projs, clusters: clusters}
+func NewWithStores(
+	database *sqlx.DB,
+	orgs store.OrganizationStore,
+	users store.UserStore,
+	projs store.ProjectStore,
+	clusters store.ComputeClusterStore,
+	allocs store.ComputeAllocationStore,
+	resources store.ComputeAllocationResourceStore,
+	resourceMappings store.ComputeAllocationResourceMappingStore,
+) *Service {
+	return &Service{
+		db:               database,
+		orgs:             orgs,
+		users:            users,
+		projs:            projs,
+		clusters:         clusters,
+		allocs:           allocs,
+		resources:        resources,
+		resourceMappings: resourceMappings,
+	}
 }
 
 // inTx runs fn inside a database transaction managed by the Service.