diff --git a/docs/architecture/service-orchestration-boundary.md b/docs/architecture/service-orchestration-boundary.md
new file mode 100644
index 0000000..6821b14
--- /dev/null
+++ b/docs/architecture/service-orchestration-boundary.md
@@ -0,0 +1,14 @@
+# Furniture Service Orchestration Boundary
+
+## Purpose
+Constrain furniture-service to orchestration responsibilities after domain extraction.
+
+## Service Responsibilities
+- Coordinate use-case flow
+- Call domain abstractions for decisions
+- Adapt transport contracts
+- Depend on Domain and DAL boundaries without direct BuildingBlock ownership
+
+## Prohibited Responsibilities
+- Owning business decision rules
+- Owning persistence decision concerns
diff --git a/docs/migration/domain-delegation-plan.md b/docs/migration/domain-delegation-plan.md
new file mode 100644
index 0000000..087b23d
--- /dev/null
+++ b/docs/migration/domain-delegation-plan.md
@@ -0,0 +1,10 @@
+# Domain Delegation Plan
+
+## Delegation Model
+- Use cases invoke furniture-domain abstractions for decision logic.
+- Service adapters retain technical mapping responsibilities.
+
+## Transition Steps
+1. Replace in-service decision branches with domain calls.
+2. Keep contract shapes stable at service boundary.
+3. Validate orchestration-only responsibilities.
diff --git a/docs/migration/orchestration-regression-checks.md b/docs/migration/orchestration-regression-checks.md
new file mode 100644
index 0000000..902f546
--- /dev/null
+++ b/docs/migration/orchestration-regression-checks.md
@@ -0,0 +1,10 @@
+# Orchestration Regression Checks
+
+## Checks
+- Service no longer contains domain decision branches.
+- Service still orchestrates required dependencies.
+- Transport contract outputs remain stable.
+
+## Evidence
+- Updated architecture docs
+- Migration mapping confirmation
diff --git a/src/Furniture.Service.Application/Adapters/FurnitureAvailabilityGrpcContractAdapter.cs b/src/Furniture.Service.Application/Adapters/FurnitureAvailabilityGrpcContractAdapter.cs
new file mode 100644
index 0000000..bbaeb4a
--- /dev/null
+++ b/src/Furniture.Service.Application/Adapters/FurnitureAvailabilityGrpcContractAdapter.cs
@@ -0,0 +1,36 @@
+using Furniture.Service.Application.Grpc;
+using Furniture.Service.Contracts.UseCases;
+
+namespace Furniture.Service.Application.Adapters;
+
+///
+/// Default adapter implementation for furniture service gRPC contract translation.
+///
+public sealed class FurnitureAvailabilityGrpcContractAdapter : IFurnitureAvailabilityGrpcContractAdapter
+{
+ ///
+ public GetFurnitureAvailabilityGrpcContract ToGrpc(GetFurnitureAvailabilityRequest request)
+ {
+ return new GetFurnitureAvailabilityGrpcContract(
+ request.FurnitureId,
+ ResolveCorrelationId(request.CorrelationId));
+ }
+
+ ///
+ public GetFurnitureAvailabilityRequest FromGrpc(GetFurnitureAvailabilityGrpcContract contract)
+ {
+ return new GetFurnitureAvailabilityRequest(
+ contract.FurnitureId,
+ ResolveCorrelationId(contract.CorrelationId));
+ }
+
+ private static string ResolveCorrelationId(string correlationId)
+ {
+ if (!string.IsNullOrWhiteSpace(correlationId))
+ {
+ return correlationId;
+ }
+
+ return $"corr-{Guid.NewGuid():N}";
+ }
+}
diff --git a/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs b/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs
deleted file mode 100644
index 2f501e5..0000000
--- a/src/Furniture.Service.Application/Adapters/IFurnitureAvailabilityContractAdapter.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using BuildingBlock.Catalog.Contracts.Products;
-using BuildingBlock.Catalog.Contracts.Responses;
-using BuildingBlock.Inventory.Contracts.Requests;
-using BuildingBlock.Inventory.Contracts.Responses;
-using Furniture.Service.Contracts.UseCases;
-
-namespace Furniture.Service.Application.Adapters;
-
-///
-/// Defines adapter boundary for furniture service contract composition.
-///
-public interface IFurnitureAvailabilityContractAdapter
-{
- ///
- /// Maps service request into an inventory capability request.
- ///
- /// Furniture availability request.
- /// Inventory lookup request.
- InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request);
-
- ///
- /// Maps service request into a catalog capability request.
- ///
- /// Furniture availability request.
- /// Catalog product request.
- ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request);
-
- ///
- /// Maps capability responses back into the service response contract.
- ///
- /// Furniture availability request.
- /// Catalog product response.
- /// Inventory lookup response.
- /// Furniture availability response.
- GetFurnitureAvailabilityResponse ToServiceResponse(
- GetFurnitureAvailabilityRequest request,
- ProductContractResponse catalogResponse,
- InventoryItemLookupResponse inventoryResponse);
-}
diff --git a/src/Furniture.Service.Application/DependencyInjection/FurnitureServiceRuntimeServiceCollectionExtensions.cs b/src/Furniture.Service.Application/DependencyInjection/FurnitureServiceRuntimeServiceCollectionExtensions.cs
new file mode 100644
index 0000000..b22c04b
--- /dev/null
+++ b/src/Furniture.Service.Application/DependencyInjection/FurnitureServiceRuntimeServiceCollectionExtensions.cs
@@ -0,0 +1,35 @@
+using Core.Blueprint.Common.DependencyInjection;
+using Furniture.DAL.DependencyInjection;
+using Furniture.Domain.Decisions;
+using Furniture.Service.Application.Adapters;
+using Furniture.Service.Application.Ports;
+using Furniture.Service.Application.UseCases;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Furniture.Service.Application.DependencyInjection;
+
+///
+/// Registers furniture-service runtime orchestration and DAL adapters.
+///
+public static class FurnitureServiceRuntimeServiceCollectionExtensions
+{
+ ///
+ /// Adds furniture-service runtime wiring aligned with blueprint runtime and furniture-dal runtime.
+ ///
+ /// Service collection.
+ /// Service collection for fluent chaining.
+ public static IServiceCollection AddFurnitureServiceRuntime(this IServiceCollection services)
+ {
+ services.AddBlueprintRuntimeCore();
+ services.AddFurnitureDalRuntime();
+ services.TryAddSingleton();
+
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/Furniture.Service.Application/Furniture.Service.Application.csproj b/src/Furniture.Service.Application/Furniture.Service.Application.csproj
index 007938b..b9b8386 100644
--- a/src/Furniture.Service.Application/Furniture.Service.Application.csproj
+++ b/src/Furniture.Service.Application/Furniture.Service.Application.csproj
@@ -5,8 +5,9 @@
enable
-
-
+
+
+
diff --git a/src/Furniture.Service.Application/Ports/CatalogProductReadPortDalAdapter.cs b/src/Furniture.Service.Application/Ports/CatalogProductReadPortDalAdapter.cs
new file mode 100644
index 0000000..35e9bfc
--- /dev/null
+++ b/src/Furniture.Service.Application/Ports/CatalogProductReadPortDalAdapter.cs
@@ -0,0 +1,38 @@
+using BuildingBlock.Catalog.Contracts.Conventions;
+using BuildingBlock.Catalog.Contracts.Products;
+using BuildingBlock.Catalog.Contracts.Responses;
+using Furniture.DAL.Contracts;
+using Furniture.DAL.Repositories;
+
+namespace Furniture.Service.Application.Ports;
+
+///
+/// Default DAL adapter for catalog product read port.
+///
+public sealed class CatalogProductReadPortDalAdapter(ICatalogRepository catalogRepository) : ICatalogProductReadPort
+{
+ ///
+ public async Task ReadProductAsync(ProductContract request)
+ {
+ var lookupRequest = new CatalogProductLookupRequest(
+ new FurnitureDalContractEnvelope(
+ request.Envelope.ContractVersion,
+ request.Envelope.CorrelationId),
+ request.ProductId);
+
+ var projectionRecord = await catalogRepository.ReadProductAsync(lookupRequest);
+ if (projectionRecord is null)
+ {
+ return new ProductContractResponse(request.Envelope, request.ProductId, string.Empty);
+ }
+
+ var responseEnvelope = new CatalogContractEnvelope(
+ projectionRecord.Envelope.ContractVersion,
+ projectionRecord.Envelope.CorrelationId);
+
+ return new ProductContractResponse(
+ responseEnvelope,
+ projectionRecord.ProductId,
+ projectionRecord.DisplayName);
+ }
+}
diff --git a/src/Furniture.Service.Application/Ports/FurnitureAvailabilityReadPortDalAdapter.cs b/src/Furniture.Service.Application/Ports/FurnitureAvailabilityReadPortDalAdapter.cs
new file mode 100644
index 0000000..f30d0b6
--- /dev/null
+++ b/src/Furniture.Service.Application/Ports/FurnitureAvailabilityReadPortDalAdapter.cs
@@ -0,0 +1,39 @@
+using BuildingBlock.Inventory.Contracts.Conventions;
+using BuildingBlock.Inventory.Contracts.Requests;
+using BuildingBlock.Inventory.Contracts.Responses;
+using Furniture.DAL.Contracts;
+using Furniture.DAL.Repositories;
+
+namespace Furniture.Service.Application.Ports;
+
+///
+/// Default DAL adapter for furniture availability read port.
+///
+public sealed class FurnitureAvailabilityReadPortDalAdapter(
+ IFurnitureRepository furnitureRepository) : IFurnitureAvailabilityReadPort
+{
+ ///
+ public async Task ReadAvailabilityAsync(InventoryItemLookupRequest request)
+ {
+ var lookupRequest = new FurnitureAvailabilityLookupRequest(
+ new FurnitureDalContractEnvelope(
+ request.Envelope.ContractVersion,
+ request.Envelope.CorrelationId),
+ request.ItemCode);
+
+ var availabilityRecord = await furnitureRepository.ReadAvailabilityAsync(lookupRequest);
+ if (availabilityRecord is null)
+ {
+ return new InventoryItemLookupResponse(request.Envelope, request.ItemCode, 0);
+ }
+
+ var responseEnvelope = new InventoryContractEnvelope(
+ availabilityRecord.Envelope.ContractVersion,
+ availabilityRecord.Envelope.CorrelationId);
+
+ return new InventoryItemLookupResponse(
+ responseEnvelope,
+ availabilityRecord.FurnitureId,
+ availabilityRecord.QuantityAvailable);
+ }
+}
diff --git a/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs b/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs
index 52acd3d..a2e4dc2 100644
--- a/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs
+++ b/src/Furniture.Service.Application/UseCases/GetFurnitureAvailabilityUseCase.cs
@@ -1,6 +1,7 @@
-using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Contracts.UseCases;
+using Furniture.Domain.Contracts;
+using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UseCases;
@@ -8,7 +9,7 @@ namespace Furniture.Service.Application.UseCases;
/// Default orchestration implementation for furniture availability lookup.
///
public sealed class GetFurnitureAvailabilityUseCase(
- IFurnitureAvailabilityContractAdapter contractAdapter,
+ IFurnitureAvailabilityDecisionService decisionService,
ICatalogProductReadPort catalogReadPort,
IFurnitureAvailabilityReadPort inventoryReadPort)
: IGetFurnitureAvailabilityUseCase
@@ -16,17 +17,25 @@ public sealed class GetFurnitureAvailabilityUseCase(
///
public async Task HandleAsync(GetFurnitureAvailabilityRequest request)
{
- var catalogRequest = contractAdapter.ToCatalogRequest(request);
- var inventoryRequest = contractAdapter.ToInventoryRequest(request);
+ var domainRequest = new FurnitureAvailabilityDecisionRequest(
+ request.FurnitureId,
+ request.CorrelationId);
+ var catalogRequest = decisionService.BuildCatalogRequest(domainRequest);
+ var inventoryRequest = decisionService.BuildInventoryRequest(domainRequest);
var catalogTask = catalogReadPort.ReadProductAsync(catalogRequest);
var inventoryTask = inventoryReadPort.ReadAvailabilityAsync(inventoryRequest);
await Task.WhenAll(catalogTask, inventoryTask);
- return contractAdapter.ToServiceResponse(
- request,
+ var domainResponse = decisionService.ComposeResponse(
+ domainRequest,
await catalogTask,
await inventoryTask);
+
+ return new GetFurnitureAvailabilityResponse(
+ domainResponse.FurnitureId,
+ domainResponse.DisplayName,
+ domainResponse.QuantityAvailable);
}
}
diff --git a/src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj b/src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj
index d236f97..c391cd2 100644
--- a/src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj
+++ b/src/Furniture.Service.Grpc/Furniture.Service.Grpc.csproj
@@ -4,6 +4,16 @@
enable
enable
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/Furniture.Service.Grpc/Program.cs b/src/Furniture.Service.Grpc/Program.cs
index ef5cbdd..4f71e79 100644
--- a/src/Furniture.Service.Grpc/Program.cs
+++ b/src/Furniture.Service.Grpc/Program.cs
@@ -1,6 +1,15 @@
+using Furniture.Service.Application.DependencyInjection;
+using Furniture.Service.Grpc.Services;
+
var builder = WebApplication.CreateBuilder(args);
-// Stage 3 skeleton: keep a single active internal protocol policy (gRPC-first).
+builder.Services.AddGrpc();
+builder.Services.AddHealthChecks();
+builder.Services.AddFurnitureServiceRuntime();
+
var app = builder.Build();
+app.MapGrpcService();
+app.MapHealthChecks("/healthz");
+
app.Run();
diff --git a/src/Furniture.Service.Grpc/Protos/furniture_runtime.proto b/src/Furniture.Service.Grpc/Protos/furniture_runtime.proto
new file mode 100644
index 0000000..785ba0f
--- /dev/null
+++ b/src/Furniture.Service.Grpc/Protos/furniture_runtime.proto
@@ -0,0 +1,20 @@
+syntax = "proto3";
+
+option csharp_namespace = "Furniture.Service.Grpc";
+
+package furniture.service.grpc;
+
+service FurnitureRuntime {
+ rpc GetFurnitureAvailability (GetFurnitureAvailabilityGrpcRequest) returns (GetFurnitureAvailabilityGrpcResponse);
+}
+
+message GetFurnitureAvailabilityGrpcRequest {
+ string furniture_id = 1;
+ string correlation_id = 2;
+}
+
+message GetFurnitureAvailabilityGrpcResponse {
+ string furniture_id = 1;
+ string display_name = 2;
+ int32 quantity_available = 3;
+}
diff --git a/src/Furniture.Service.Grpc/Services/FurnitureRuntimeGrpcService.cs b/src/Furniture.Service.Grpc/Services/FurnitureRuntimeGrpcService.cs
new file mode 100644
index 0000000..4d2bb1e
--- /dev/null
+++ b/src/Furniture.Service.Grpc/Services/FurnitureRuntimeGrpcService.cs
@@ -0,0 +1,39 @@
+using Grpc.Core;
+using Furniture.Service.Application.Adapters;
+using Furniture.Service.Application.Grpc;
+using Furniture.Service.Application.UseCases;
+
+namespace Furniture.Service.Grpc.Services;
+
+///
+/// Internal gRPC endpoint implementation for furniture runtime operations.
+///
+public sealed class FurnitureRuntimeGrpcService(
+ IGetFurnitureAvailabilityUseCase getFurnitureAvailabilityUseCase,
+ IFurnitureAvailabilityGrpcContractAdapter grpcContractAdapter) : FurnitureRuntime.FurnitureRuntimeBase
+{
+ ///
+ /// Executes furniture availability lookup through service use-case orchestration.
+ ///
+ /// gRPC availability request.
+ /// gRPC server call context.
+ /// gRPC availability response.
+ public override async Task GetFurnitureAvailability(
+ GetFurnitureAvailabilityGrpcRequest request,
+ ServerCallContext context)
+ {
+ var grpcContract = new GetFurnitureAvailabilityGrpcContract(
+ request.FurnitureId,
+ request.CorrelationId);
+
+ var useCaseRequest = grpcContractAdapter.FromGrpc(grpcContract);
+ var useCaseResponse = await getFurnitureAvailabilityUseCase.HandleAsync(useCaseRequest);
+
+ return new GetFurnitureAvailabilityGrpcResponse
+ {
+ FurnitureId = useCaseResponse.FurnitureId,
+ DisplayName = useCaseResponse.DisplayName,
+ QuantityAvailable = useCaseResponse.QuantityAvailable
+ };
+ }
+}
diff --git a/tests/Furniture.Service.Application.UnitTests/Furniture.Service.Application.UnitTests.csproj b/tests/Furniture.Service.Application.UnitTests/Furniture.Service.Application.UnitTests.csproj
index f2fe7f7..a5758c1 100644
--- a/tests/Furniture.Service.Application.UnitTests/Furniture.Service.Application.UnitTests.csproj
+++ b/tests/Furniture.Service.Application.UnitTests/Furniture.Service.Application.UnitTests.csproj
@@ -7,6 +7,7 @@
+
diff --git a/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs b/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs
index 3317df7..a36224c 100644
--- a/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs
+++ b/tests/Furniture.Service.Application.UnitTests/GetFurnitureAvailabilityUseCaseTests.cs
@@ -4,10 +4,11 @@ using BuildingBlock.Catalog.Contracts.Responses;
using BuildingBlock.Inventory.Contracts.Conventions;
using BuildingBlock.Inventory.Contracts.Requests;
using BuildingBlock.Inventory.Contracts.Responses;
-using Furniture.Service.Application.Adapters;
using Furniture.Service.Application.Ports;
using Furniture.Service.Application.UseCases;
using Furniture.Service.Contracts.UseCases;
+using Furniture.Domain.Contracts;
+using Furniture.Domain.Decisions;
namespace Furniture.Service.Application.UnitTests;
@@ -16,10 +17,10 @@ public class GetFurnitureAvailabilityUseCaseTests
[Fact]
public async Task HandleAsync_WhenCalled_DelegatesToReadPort()
{
- var adapter = new FakeFurnitureAvailabilityContractAdapter();
+ var decisionService = new FakeFurnitureAvailabilityDecisionService();
var catalogPort = new FakeCatalogProductReadPort();
var inventoryPort = new FakeFurnitureAvailabilityReadPort();
- var useCase = new GetFurnitureAvailabilityUseCase(adapter, catalogPort, inventoryPort);
+ var useCase = new GetFurnitureAvailabilityUseCase(decisionService, catalogPort, inventoryPort);
var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
@@ -28,9 +29,9 @@ public class GetFurnitureAvailabilityUseCaseTests
Assert.Equal(10, response.QuantityAvailable);
}
- private sealed class FakeFurnitureAvailabilityContractAdapter : IFurnitureAvailabilityContractAdapter
+ private sealed class FakeFurnitureAvailabilityDecisionService : IFurnitureAvailabilityDecisionService
{
- public ProductContract ToCatalogRequest(GetFurnitureAvailabilityRequest request)
+ public ProductContract BuildCatalogRequest(FurnitureAvailabilityDecisionRequest request)
{
return new ProductContract(
new CatalogContractEnvelope("1.0.0", request.CorrelationId),
@@ -38,19 +39,19 @@ public class GetFurnitureAvailabilityUseCaseTests
string.Empty);
}
- public InventoryItemLookupRequest ToInventoryRequest(GetFurnitureAvailabilityRequest request)
+ public InventoryItemLookupRequest BuildInventoryRequest(FurnitureAvailabilityDecisionRequest request)
{
return new InventoryItemLookupRequest(
new InventoryContractEnvelope("1.0.0", request.CorrelationId),
request.FurnitureId);
}
- public GetFurnitureAvailabilityResponse ToServiceResponse(
- GetFurnitureAvailabilityRequest request,
+ public FurnitureAvailabilityDecisionResponse ComposeResponse(
+ FurnitureAvailabilityDecisionRequest request,
ProductContractResponse catalogResponse,
InventoryItemLookupResponse inventoryResponse)
{
- return new GetFurnitureAvailabilityResponse(
+ return new FurnitureAvailabilityDecisionResponse(
request.FurnitureId,
catalogResponse.DisplayName,
inventoryResponse.QuantityAvailable);
diff --git a/tests/Furniture.Service.Application.UnitTests/RuntimeWiringTests.cs b/tests/Furniture.Service.Application.UnitTests/RuntimeWiringTests.cs
new file mode 100644
index 0000000..18e861e
--- /dev/null
+++ b/tests/Furniture.Service.Application.UnitTests/RuntimeWiringTests.cs
@@ -0,0 +1,51 @@
+using Furniture.Service.Application.Adapters;
+using Furniture.Service.Application.DependencyInjection;
+using Furniture.Service.Application.Grpc;
+using Furniture.Service.Application.UseCases;
+using Furniture.Service.Contracts.UseCases;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Furniture.Service.Application.UnitTests;
+
+public class RuntimeWiringTests
+{
+ [Fact]
+ public async Task AddFurnitureServiceRuntime_WhenInvoked_ResolvesUseCase()
+ {
+ var services = new ServiceCollection();
+ services.AddFurnitureServiceRuntime();
+
+ using var provider = services.BuildServiceProvider();
+ var useCase = provider.GetRequiredService();
+
+ var response = await useCase.HandleAsync(new GetFurnitureAvailabilityRequest("FUR-001", "corr-123"));
+
+ Assert.Equal("FUR-001", response.FurnitureId);
+ Assert.Equal(8, response.QuantityAvailable);
+ }
+
+ [Fact]
+ public void FurnitureAvailabilityGrpcContractAdapter_WhenMapped_PreservesValues()
+ {
+ var adapter = new FurnitureAvailabilityGrpcContractAdapter();
+ var request = new GetFurnitureAvailabilityRequest("FUR-002", "corr-456");
+
+ var grpcContract = adapter.ToGrpc(request);
+ var roundtrip = adapter.FromGrpc(grpcContract);
+
+ Assert.Equal("FUR-002", roundtrip.FurnitureId);
+ Assert.Equal("corr-456", roundtrip.CorrelationId);
+ }
+
+ [Fact]
+ public void FurnitureAvailabilityGrpcContractAdapter_WhenCorrelationMissing_GeneratesCorrelation()
+ {
+ var adapter = new FurnitureAvailabilityGrpcContractAdapter();
+ var grpcContract = new GetFurnitureAvailabilityGrpcContract("FUR-003", string.Empty);
+
+ var mapped = adapter.FromGrpc(grpcContract);
+
+ Assert.Equal("FUR-003", mapped.FurnitureId);
+ Assert.NotEmpty(mapped.CorrelationId);
+ }
+}